go generate

@ehrt74/go-generate-89b20a27f7f9"">https://medium.com/@ehrt74/go-generate-89b20a27f7f9

Edouard Tavinor

Hello! this is a short post about using go generate to generate code.

go generate is a tool that comes with the go command. You write go generate directives in your source code and then run the directives by calling go generate on the command line (or using your IDE). Here’s an example main.go:

  1. package main
  2. import "fmt"
  3. //go:generate ./command.sh
  4. func main() {
  5. fmt.Println("if you type 'go generate' in this directory command.sh will be run")
  6. }

And here’s a minimal command.sh in the same directory as main.go:

  1. #!/bin/bashecho "hello world!"

Now I can run the code as follows:

  1. $ ls
  2. main.go command.sh
  3. $ chmod +x command.sh
  4. $ go generate
  5. hello world!

go generate also accepts a couple of flags. The only one I use regularly is -x, which prints a list of the commands as they are executed to STDOUT.

  1. $ go generate -x
  2. ./command.sh
  3. hello world!

And that’s the basic usage of go generate. Now to make it do something useful. Let’s say we want to work on slices of struct pointers. We have the following code in the file ./types/person.go:

  1. package types
  2. import (
  3. "fmt"
  4. )
  5. type Person struct {
  6. Name string
  7. Age int
  8. }
  9. func (p *Person)String() string {
  10. return fmt.Sprintf("%v (%v)", p.Name, p.Age)
  11. }
  12. type PersonToBool func(*Person) bool
  13. type PersonList []*Personfunc (pl PersonList)Filter(f PersonToBool) PersonList {
  14. var ret []*Person
  15. for _, p := range pl {
  16. if f(p) {
  17. ret = append(ret, p)
  18. }
  19. }
  20. return ret
  21. }

And this is our new main.go:

  1. package main
  2. import (
  3. "fmt"
  4. "./types"
  5. )
  6. func main() {
  7. var pl types.PersonList
  8. pl = append(pl, &types.Person{Name:"Jane", Age:32})
  9. pl = append(pl, &types.Person{Name:"Ed", Age:27})
  10. pl2 := pl.Filter( func(p *types.Person) bool {
  11. return p.Age>30
  12. }) for _, p := range pl2 {
  13. fmt.Println(p)
  14. }
  15. }

We run the code:

  1. $ ls
  2. main.go types
  3. $ go run
  4. Jane (32)

Let’s say we also have an Address struct. This is the code for ./types/address.go:

  1. package types
  2. import "fmt"
  3. type Address struct {
  4. Street string
  5. Town string
  6. }
  7. func (a *Address)String() string {
  8. return fmt.Sprintf("%v\n%v", a.Street, a.Town)
  9. }
  10. type AddressList []*Address
  11. type AddressToBool func(*Address) bool
  12. func (al AddressList)Filter(f AddressToBool) AddressList {
  13. var ret AddressList
  14. for _, a := range al {
  15. if f(a) {
  16. ret = append(ret, a)
  17. }
  18. }
  19. return ret
  20. }

As you can see, it looks almost exactly like ./types/person.go. Is there a way we can make a generic Filter method?

We can generate the code by calling another program. Here’s the code for ./types/newList.sh:

  1. #!/bin/sh
  2. TYPE=$1
  3. cat > ${TYPE}List.go <<EOLpackage typestype ${TYPE}List []*${TYPE}
  4. type ${TYPE}ToBool func(*${TYPE}) bool
  5. func (al ${TYPE}List)Filter(f ${TYPE}ToBool) ${TYPE}List {
  6. var ret ${TYPE}List
  7. for _, a := range al {
  8. if f(a) {
  9. ret = append(ret, a)
  10. }
  11. }
  12. return ret
  13. }
  14. EOL

And our ./types/person.go becomes:

  1. package types
  2. import "fmt"
  3. //go:generate ./newList.sh
  4. Person type Person struct {
  5. Name string
  6. Age int
  7. }
  8. func (p *Person)String() string {
  9. return fmt.Sprintf("%v (%v)", p.Name, p.Age)
  10. }

Now we can run

  1. $ cd types
  2. $ chmod +x newList.sh
  3. $ go generate
  4. $ cd ..
  5. $ go run
  6. jane (32)

So it all works! But there’s a problem. With a very simple shell script, debugging the code to generate the PersonList struct and the Filter method will be easy enough, but it will get difficult the longer the code is. It would be better to create a dummy struct and use a standard go file. Let’s create a file ./types/list.tmpl.go:

  1. package types
  2. type DUMMYTYPEList []*DUMMYTYPE
  3. type DUMMYTYPEToBool func(*DUMMYTYPE) bool
  4. func (al DUMMYTYPEList)Filter(f DUMMYTYPEToBool) DUMMYTYPEList {
  5. var ret DUMMYTYPEList
  6. for _, a := range al {
  7. if f(a) {
  8. ret = append(ret, a)
  9. }
  10. }
  11. return ret
  12. }

And a small file ./types/dummyType.go:

  1. package types
  2. type DUMMYTYPE interface{}

Now we get syntax highlighting in our IDE! We just have to write a short shell command to exchange DUMMYTYPE with the name of another type. This is the new ./types/newList.sh:

  1. #!/bin/sh
  2. TYPE=$1
  3. cat list.tmpl.go | sed -e 's/DUMMYTYPE/'${TYPE}'/g' > ${TYPE}List.go

We can run the code as above:

  1. $ cd types
  2. $ go generate
  3. $ cd ..
  4. $ go run
  5. Jane (32)

Of course, you could extend the shell script to allow for multiple class arguments for methods like MapToSomeOtherType, or you could use a totally different language to write the commands called by go generate. Unfortunately I can’t see a way to use go templates to do this, because go templates are not valid go source code files.

I hope this helps somebody :)

ft_authoradmin  ft_create_time2020-01-24 00:20
 ft_update_time2020-01-24 00:27