blob: d2e2da3c39f933fba4c01f794798510577136a76 [file] [log] [blame]
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