├── go.mod ├── main.go ├── .gitignore ├── Makefile ├── cli.go ├── README.md ├── go.sum └── refreturn.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dominikbraun/refreturn 2 | 3 | go 1.12 4 | 5 | require github.com/spf13/cobra v0.0.5 6 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | var version = "UNDEFINED" 8 | 9 | func main() { 10 | if err := rootCommand(version).Execute(); err != nil { 11 | log.Fatal(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Dependency directories 12 | # vendor/ 13 | 14 | # IDE files 15 | .idea 16 | .vscode 17 | 18 | # refreturn binary 19 | refreturn 20 | refreturn.exe 21 | cmd/*/__debug_bin 22 | .target/* -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(git describe --tags) 2 | 3 | linux: 4 | go build -o ./target/refreturn -ldflags="-X main.version=${VERSION}" ./main.go 5 | mac: 6 | go build -o ./target/refreturn -ldflags="-X main.version=${VERSION}" ./main.go 7 | windows: 8 | go build -o ./target/refreturn.exe -ldflags="-X main.version=${VERSION}" ./main.go 9 | clean: 10 | rm -rf ./target 11 | all: linux mac windows -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const ( 8 | shortHelp = `Find functions that return a reference and cause allocations.` 9 | longHelp = `refreturn finds all Go functions in a directory tree that return a 10 | reference and cause a potential unnecessary heap allocation.` 11 | ) 12 | 13 | // rootCommand generates the top-level `refreturn` command. 14 | func rootCommand(version string) *cobra.Command { 15 | rootCommand := &cobra.Command{ 16 | Use: "refreturn ", 17 | Version: version, 18 | Short: shortHelp, 19 | Long: longHelp, 20 | Args: cobra.ExactArgs(1), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | path := args[0] 23 | return Run(path) 24 | }, 25 | } 26 | 27 | return rootCommand 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # refreturn 2 | > Find functions that return a reference and cause allocations. 3 | 4 | When a function allocates a value and returns a reference to it, the value has to escape to the heap. This is slower and puts pressure on the garbage collector. 5 | 6 | **This may be optimized: You can avoid the heap allocation and allow the function to be inlined.** 7 | 8 | --- 9 | 10 | ### Example: a simple constructor 11 | 12 | ```go 13 | struct Coffee { 14 | Type string 15 | } 16 | 17 | func New() *Coffee { 18 | c := Coffee{ 19 | Type: "espresso" 20 | } 21 | return &c 22 | } 23 | ``` 24 | 25 | 0. [Download refreturn](https://github.com/dominikbraun/refreturn/releases) and copy the binary into your project's root for example. 26 | 1. Run `./refreturn ` and you'll see that `New` returns a reference. 27 | 2. Check if the returned value is being created in the function. 28 | 3. This is true for our `c` variable. 29 | 4. Optimize the function like so: 30 | 31 | ```go 32 | func New() *Coffee { 33 | var c Coffee 34 | return new(&c) 35 | } 36 | 37 | func new(c *Coffee) *Coffee { 38 | c.Type = "espresso" 39 | return c 40 | } 41 | ``` 42 | 43 | `New()` is now merely a wrapper which allocates the instance. The "real work" will be done in `new()`. 44 | 45 | **This will allow mid-stack inlining as described in [this blog post](https://blog.filippo.io/efficient-go-apis-with-the-inliner/).** 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 10 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 11 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 12 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 13 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 14 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 15 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 18 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 19 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 20 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 21 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 22 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 23 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 24 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 25 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 26 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 27 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 28 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 29 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 30 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 34 | -------------------------------------------------------------------------------- /refreturn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | const ( 15 | numWorkers int = 4 16 | fileExtension string = ".go" 17 | ) 18 | 19 | // Run executes refreturn. It is in charge of spawning all worker 20 | // routines, sending the files to be processed through a channel 21 | // and gracefully stopping all workers. 22 | // 23 | // At the moment, the results are not queued but printed directly 24 | // by the workers instead. This may change in the future. 25 | func Run(dir string) error { 26 | var wg sync.WaitGroup 27 | jobQueue := make(chan string) 28 | 29 | for i := 0; i < numWorkers; i++ { 30 | wg.Add(1) 31 | 32 | go func() { 33 | worker := &Worker{} 34 | _ = worker.readFromQueue(jobQueue, &wg) 35 | }() 36 | } 37 | 38 | if err := sendFiles(dir, jobQueue); err != nil { 39 | return err 40 | } 41 | 42 | close(jobQueue) 43 | wg.Wait() 44 | 45 | return nil 46 | } 47 | 48 | // sendFiles sends all files matching the configured file extension 49 | // through the jobQueue channel. 50 | func sendFiles(dir string, jobQueue chan<- string) error { 51 | return filepath.Walk(dir, func(path string, _ os.FileInfo, err error) error { 52 | if err != nil { 53 | return err 54 | } 55 | if strings.HasSuffix(path, fileExtension) { 56 | jobQueue <- path 57 | } 58 | return nil 59 | }) 60 | } 61 | 62 | type Worker struct{} 63 | 64 | // readFromQueue pops off items from the job queue sequentially. 65 | // Each queue item will be passed to findAllocationsInFile. 66 | func (w *Worker) readFromQueue(jobs <-chan string, wg *sync.WaitGroup) error { 67 | for path := range jobs { 68 | if err := w.findAllocationsInFile(path); err != nil { 69 | return err 70 | } 71 | } 72 | wg.Done() 73 | 74 | return nil 75 | } 76 | 77 | // findAllocationsInFile parses a source code file and walks through 78 | // its syntax tree. In doing so, a custom node visitor will check if 79 | // a node is a function that returns a pointer. All matching nodes 80 | // will be sent to a dedicated channel. 81 | func (w *Worker) findAllocationsInFile(path string) error { 82 | fileSet := token.NewFileSet() 83 | 84 | file, err := parser.ParseFile(fileSet, path, nil, parser.AllErrors) 85 | if err != nil { 86 | return nil 87 | } 88 | 89 | visitor := Visitor{ 90 | matches: make(chan Node), 91 | } 92 | 93 | go func() { 94 | ast.Walk(visitor, file) 95 | close(visitor.matches) 96 | }() 97 | 98 | // Iterate over all matches and print the file information for 99 | // each match. This should be outsourced to an own component. 100 | for match := range visitor.matches { 101 | pos := fileSet.PositionFor(match.Position, false) 102 | fn := match.Identifier.Name 103 | 104 | fmt.Printf("%s: %s\n", pos, fn) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | // Visitor satisfies the ast.Visitor interface and is used by for 111 | // inspecting every AST node using ast.Walk(). 112 | type Visitor struct { 113 | matches chan Node 114 | filter func(node ast.Node) bool 115 | } 116 | 117 | // Node represents an AST node with an identifier. 118 | type Node struct { 119 | Position token.Pos 120 | Identifier *ast.Ident 121 | } 122 | 123 | // Visit checks the type a given AST node `n`. If the node is a 124 | // function declaration, a return type check is performed. 125 | func (v Visitor) Visit(node ast.Node) ast.Visitor { 126 | if node == nil { 127 | return nil 128 | } 129 | 130 | switch decl := node.(type) { 131 | case *ast.FuncDecl: 132 | if containsReference(decl.Type.Results) { 133 | v.matches <- Node{ 134 | Position: decl.Pos(), 135 | Identifier: decl.Name, 136 | } 137 | } 138 | } 139 | return v 140 | } 141 | 142 | // containsReference determines if one of a function's return types 143 | // is a reference. Each return type is an entry in the FieldList. 144 | func containsReference(fieldList *ast.FieldList) bool { 145 | if fieldList == nil { 146 | return false 147 | } 148 | for _, f := range fieldList.List { 149 | if _, ok := f.Type.(*ast.StarExpr); ok { 150 | return true 151 | } 152 | } 153 | return false 154 | } 155 | --------------------------------------------------------------------------------