| Gophers With Hammers |
| |
| Josh Bleecher Snyder |
| PayPal |
| josharian@gmail.com |
| @offbymany |
| |
| |
| * Go was designed with tools in mind. (Rob Pike) |
| |
| * Designed with tools in mind |
| |
| Simple, regular syntax |
| Simple semantics |
| Batteries included |
| |
| * Tools everywhere |
| |
| - go |
| - gofmt, goimports |
| - godoc |
| - go test [-cover] [-race] |
| - go vet |
| - gofix, gofmt -r, eg |
| - oracle |
| - golint |
| - godep |
| |
| and more... |
| |
| * go command |
| |
| $ go list -f '{{.Deps}}' bytes |
| [errors io runtime sync sync/atomic unicode unicode/utf8 unsafe] |
| |
| * gofmt |
| |
| from |
| |
| for{ |
| fmt.Println( "I feel pretty." ); |
| } |
| |
| to |
| |
| for { |
| fmt.Println("I feel pretty.") |
| } |
| |
| * godoc |
| |
| $ godoc strings Repeat |
| func Repeat(s string, count int) string |
| Repeat returns a new string consisting of count copies of the string s. |
| |
| * go vet |
| |
| Oops |
| |
| if suffix != ".md" || suffix != ".markdown" { |
| |
| Flagged |
| |
| suspect or: suffix != ".md" || suffix != ".markdown" |
| |
| * go tool cover -mode=set |
| |
| func Repeat(s string, count int) string { |
| b := make([]byte, len(s)*count) |
| bp := 0 |
| for i := 0; i < count; i++ { |
| bp += copy(b[bp:], s) |
| } |
| return string(b) |
| } |
| |
| to |
| |
| func Repeat(s string, count int) string { |
| GoCover.Count[0] = 1 |
| b := make([]byte, len(s)*count) |
| bp := 0 |
| for i := 0; i < count; i++ { |
| GoCover.Count[2] = 1 |
| bp += copy(b[bp:], s) |
| } |
| GoCover.Count[1] = 1 |
| return string(b) |
| } |
| |
| * go test -cover |
| |
| $ go test -coverprofile=c.out strings |
| ok strings 0.455s coverage: 96.9% of statements |
| |
| $ go tool cover -func=c.out |
| strings/reader.go: Len 66.7% |
| strings/reader.go: Read 100.0% |
| strings/reader.go: ReadAt 100.0% |
| strings/reader.go: ReadByte 100.0% |
| strings/reader.go: UnreadByte 100.0% |
| strings/reader.go: ReadRune 100.0% |
| strings/reader.go: UnreadRune 100.0% |
| strings/reader.go: Seek 90.9% |
| strings/reader.go: WriteTo 83.3% |
| ... |
| |
| $ go tool cover -html=c.out |
| # opens a browser window, shows line-by-line coverage |
| |
| |
| * Tools to make tools |
| |
| - text/template |
| - go/build |
| - go/doc |
| - go/format |
| - go/{parser,token,ast,printer} |
| - go.tools/go/types and friends |
| |
| and more... |
| |
| * Hammers are fun! |
| |
| # Why to write your own tools: Fun, learning, profit |
| |
| * impl |
| |
| Generate implementation stubs given an interface. |
| |
| go get github.com/josharian/impl |
| |
| Generate |
| |
| $ impl 'f *File' io.Reader |
| func (f *File) Read(p []byte) (n int, err error) { |
| } |
| |
| from |
| |
| package io |
| |
| type Reader interface { |
| Read(p []byte) (n int, err error) |
| } |
| |
| * impl |
| |
| Generate |
| |
| $ impl 'f *File' io.ReadWriter |
| func (f *File) Read(p []byte) (n int, err error) { |
| } |
| |
| func (f *File) Write(p []byte) (n int, err error) { |
| } |
| |
| from |
| |
| package io |
| |
| type ReadWriter interface { |
| Reader |
| Writer |
| } |
| |
| * impl |
| |
| Generate |
| |
| $ impl 'c *Ctx' http.Handler |
| func (c *Ctx) ServeHTTP(http.ResponseWriter, *http.Request) { |
| } |
| |
| from |
| |
| package http |
| |
| type Handler interface { |
| ServeHTTP(ResponseWriter, *Request) |
| } |
| |
| * Plan |
| |
| *Find*import*path*and*interface*name* |
| |
| http.Handler ⇒ net/http, Handler |
| |
| Parse interface |
| |
| net/http, Handler ⇒ {{"ServeHTTP", {{"", "http.ResponseWriter"}, {"", "*http.Request"}}, {}}}} |
| |
| Generate output |
| |
| {{"ServeHTTP", {{"", "http.ResponseWriter"}, {"", "*http.Request"}}, {}}}} ⇒ profit! |
| |
| * goimports ftw |
| |
| import "golang.org/x/tools/imports" |
| |
| .play hammers/importpath.go /func main/,/^}/ |
| |
| * Hello, AST |
| |
| *ast.File { |
| . Package: 1:1 |
| . Name: *ast.Ident { |
| . . NamePos: 1:9 |
| . . Name: "hack" |
| . } |
| . Decls: []ast.Decl (len = 2) { |
| . . 0: *ast.GenDecl { |
| . . . TokPos: 1:15 |
| . . . Tok: import |
| . . . Lparen: - |
| . . . Specs: []ast.Spec (len = 1) { |
| . . . . 0: *ast.ImportSpec { |
| . . . . . Path: *ast.BasicLit { |
| . . . . . . ValuePos: 1:22 |
| . . . . . . Kind: STRING |
| . . . . . . Value: "\"net/http\"" |
| . . . . . } |
| |
| [truncated] |
| |
| |
| * Extract the import path |
| |
| import ( |
| "go/parser" |
| "go/token" |
| ) |
| |
| .play hammers/extractpath.go /func main/,/^}/ |
| |
| * Extract the interface name |
| |
| import "go/ast" |
| |
| .play hammers/extractiface.go /func main/,/^}/ |
| |
| A `GenDecl` can have many `Specs` |
| |
| var ( |
| r io.Reader |
| w io.Writer |
| ) |
| |
| * Plan |
| |
| Find import path and interface name |
| |
| http.Handler ⇒ net/http, Handler |
| |
| *Parse*interface* |
| |
| net/http, Handler ⇒ {{"ServeHTTP", {{"", "http.ResponseWriter"}, {"", "*http.Request"}}, {}}}} |
| |
| Generate output |
| |
| {{"ServeHTTP", {{"", "http.ResponseWriter"}, {"", "*http.Request"}}, {}}}} ⇒ profit! |
| |
| * Data structures |
| |
| Represent |
| |
| Read(p []byte) (n int, err error) |
| |
| as |
| |
| Func{ |
| Name: "Read", |
| Params: []Param{{Name: "p", Type: "[]byte"}}, |
| Res: []Param{ |
| {Name: "n", Type: "int"}, |
| {Name: "err", Type: "error"}, |
| }, |
| }, |
| |
| * Data structures |
| |
| .code hammers/types.go /type Func/,/^}/ |
| .code hammers/types.go /type Param/,/^}/ |
| |
| * Find the code |
| |
| import "go/build" |
| |
| .play hammers/findthecode.go /func main/,/^}/ |
| |
| * Find the interface declaration |
| |
| import "go/printer" |
| |
| .play hammers/findtheifacedecl.go /func main/,/^}/ |
| |
| * Extract function names |
| |
| No name? It's an embedded interface. Recurse. |
| |
| type ByteScanner interface { |
| ByteReader |
| UnreadByte() error |
| } |
| |
| * Extract params and results |
| |
| No name? Just use `""`. |
| |
| type ByteWriter interface { |
| WriteByte(c byte) error |
| } |
| |
| * Qualify types |
| |
| Types can be arbitrarily complicated. |
| |
| type CrazyGopher interface { |
| CrazyGoph(int) func(chan<- [32]byte, map[string]int64) ([]rune, error) |
| } |
| |
| And we need to rewrite some of them. |
| |
| int ⇒ int |
| *Request ⇒ *http.Request |
| io.Reader ⇒ io.Reader |
| func(io.Reader, chan map[S][]*T) ⇒ func(io.Reader, chan map[foo.S][]*foo.T)) |
| |
| * Qualify types |
| |
| .play hammers/fulltype.go /func main/,/end main/ |
| |
| * Plan |
| |
| Find import path and interface name |
| |
| http.Handler ⇒ net/http, Handler |
| |
| Parse interface |
| |
| net/http, Handler ⇒ {{"ServeHTTP", {{"", "http.ResponseWriter"}, {"", "*http.Request"}}, {}}}} |
| |
| *Generate*output* |
| |
| {{"ServeHTTP", {{"", "http.ResponseWriter"}, {"", "*http.Request"}}, {}}}} ⇒ profit! |
| |
| * Method type |
| |
| .code hammers/types.go /type Method/,/^}/ |
| .code hammers/types.go /type Func/,/^}/ |
| .code hammers/types.go /type Param/,/^}/ |
| |
| * Use text/template |
| |
| .play hammers/codegen.go /func main/,/^}/ |
| |
| # Don't generate an AST. It's a lot of work, and Go is its own DSL. |
| |
| * Ugly is ok |
| |
| import "go/format" |
| |
| .play hammers/format.go /func main/,/^}/ |
| |
| * Great success |
| |
| Full code plus tests at `github.com/josharian/impl` |
| |
| * Tips |
| |
| Use `go`get`-d` to download lots of code from `godoc.org/-/index`. (Don't forget to set a temporary `GOPATH`!) |
| |
| Use (and improve) `github.com/yuroyoro/goast-viewer`. |
| |
| You don't have to generate all the code. And generating data is even better. |
| |
| The `go/ast` docs are your friend. |
| |
| `go.tools/go/types` is powerful. |
| |
| `go`generate` is coming. |
| |
| |
| * Nails! |
| |
| - Break up long strings |
| - Enums and flags to Stringers |
| - Dynamic code analysis |
| - Vet checks |
| - Reflect ⇒ codegen |
| - Convention-based http dispatch |
| - Detect "last line" copy/paste bugs |
| - AST-aware diff, merge, blame; automated fork analysis |
| - Machine learning models of ASTs: anomaly detection, bug-prone code detection |
| - Code fingerprinting |
| - Examine usage patterns |
| - Compiler stress tests |
| |