Help us understand the problem. What is going on with this article?

go generate を使える自動生成系ライブラリを触ってみる

More than 1 year has passed since last update.

それぞれ、 go generate のbefore <-> afterを載せていく

Stringer

$ go get golang.org/x/tools/cmd/stringer

before

package gen

//go:generate stringer -type=Pill
type Pill int

const (  
    Placebo Pill = iota
    Aspirin
    Ibuprofen
    Paracetamol
)

after

// Code generated by "stringer -type=Pill"; DO NOT EDIT.

package gen

import "strconv"

const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"

var _Pill_index = [...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
    if i < 0 || i >= Pill(len(_Pill_index)-1) {
        return "Pill(" + strconv.FormatInt(int64(i), 10) + ")"
    }
    return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}

一応試す

package main

import (
    "fmt"

    "github.com/smith-30/gopg/gen"
)

func main() {
    fmt.Printf("%d\n", gen.Placebo)
    fmt.Println(gen.Placebo)
}

結果

0
Placebo

jsonenums

$ go get github.com/campoy/jsonenums

before

package gen

//go:generate jsonenums -type=Pill
//go:generate stringer -type=Pill
type Pill int

const (
    Placebo Pill = iota
    Aspirin
    Ibuprofen
    Paracetamol
)

after

// generated by jsonenums -type=Pill; DO NOT EDIT

package gen

import (
    "encoding/json"
    "fmt"
)

var (
    _PillNameToValue = map[string]Pill{
        "Placebo":     Placebo,
        "Aspirin":     Aspirin,
        "Ibuprofen":   Ibuprofen,
        "Paracetamol": Paracetamol,
    }

    _PillValueToName = map[Pill]string{
        Placebo:     "Placebo",
        Aspirin:     "Aspirin",
        Ibuprofen:   "Ibuprofen",
        Paracetamol: "Paracetamol",
    }
)

func init() {
    var v Pill
    if _, ok := interface{}(v).(fmt.Stringer); ok {
        _PillNameToValue = map[string]Pill{
            interface{}(Placebo).(fmt.Stringer).String():     Placebo,
            interface{}(Aspirin).(fmt.Stringer).String():     Aspirin,
            interface{}(Ibuprofen).(fmt.Stringer).String():   Ibuprofen,
            interface{}(Paracetamol).(fmt.Stringer).String(): Paracetamol,
        }
    }
}

// MarshalJSON is generated so Pill satisfies json.Marshaler.
func (r Pill) MarshalJSON() ([]byte, error) {
    if s, ok := interface{}(r).(fmt.Stringer); ok {
        return json.Marshal(s.String())
    }
    s, ok := _PillValueToName[r]
    if !ok {
        return nil, fmt.Errorf("invalid Pill: %d", r)
    }
    return json.Marshal(s)
}

// UnmarshalJSON is generated so Pill satisfies json.Unmarshaler.
func (r *Pill) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return fmt.Errorf("Pill should be a string, got %s", data)
    }
    v, ok := _PillNameToValue[s]
    if !ok {
        return fmt.Errorf("invalid Pill %q", s)
    }
    *r = v
    return nil
}

試す

package main

import (
    "encoding/json"
    "fmt"

    "github.com/smith-30/gopg/gen"
)

func main() {
    s := gen.Sample{
        P: gen.Placebo,
    }
    b, _ := json.Marshal(s)
    fmt.Printf("%s\n", b)
}
{"P":"Placebo"}

いいですね、楽。

enumer

$ go get github.com/alvaroloes/enumer

最後は上記2つをまとめてやってくれるようなやつです

before

package gen

//go:generate enumer -type=Pill -json
type Pill int

const (
    Placebo Pill = iota
    Aspirin
    Ibuprofen
    Paracetamol
)

after

// Code generated by "enumer -type=Pill -json"; DO NOT EDIT.

package gen

import (
    "encoding/json"
    "fmt"
)

const _PillName = "PlaceboAspirinIbuprofenParacetamol"

var _PillIndex = [...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
    if i < 0 || i >= Pill(len(_PillIndex)-1) {
        return fmt.Sprintf("Pill(%d)", i)
    }
    return _PillName[_PillIndex[i]:_PillIndex[i+1]]
}

var _PillValues = []Pill{0, 1, 2, 3}

var _PillNameToValueMap = map[string]Pill{
    _PillName[0:7]:   0,
    _PillName[7:14]:  1,
    _PillName[14:23]: 2,
    _PillName[23:34]: 3,
}

// PillString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func PillString(s string) (Pill, error) {
    if val, ok := _PillNameToValueMap[s]; ok {
        return val, nil
    }
    return 0, fmt.Errorf("%s does not belong to Pill values", s)
}

// PillValues returns all values of the enum
func PillValues() []Pill {
    return _PillValues
}

// IsAPill returns "true" if the value is listed in the enum definition. "false" otherwise
func (i Pill) IsAPill() bool {
    for _, v := range _PillValues {
        if i == v {
            return true
        }
    }
    return false
}

// MarshalJSON implements the json.Marshaler interface for Pill
func (i Pill) MarshalJSON() ([]byte, error) {
    return json.Marshal(i.String())
}

// UnmarshalJSON implements the json.Unmarshaler interface for Pill
func (i *Pill) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return fmt.Errorf("Pill should be a string, got %s", data)
    }

    var err error
    *i, err = PillString(s)
    return err
}

stringやjson系の他にこんなのが追加されています。
IsA~()は書いてません

package main

import (
    "fmt"

    "github.com/smith-30/gopg/gen"
)

func main() {
    fmt.Printf("%#v\n", gen.PillValues())
    p, _ := gen.PillString("Placebo")
    fmt.Printf("%#v\n", p)
    _, err := gen.PillString("hoge")
    fmt.Printf("%#v\n", err)
}
[]gen.Pill{0, 1, 2, 3}
0
&errors.errorString{s:"hoge does not belong to Pill values"}

mock

go get github.com/golang/mock

before

package gen

//go:generate mockgen -source=repo.go -destination=./mock_gen/mock_repo.go
type Repository interface {
    Delete() error
}

type SampleRepository struct {
}

func (a *SampleRepository) Delete() error {
    return nil
}

after

// Code generated by MockGen. DO NOT EDIT.
// Source: repo.go

// Package mock_gen is a generated GoMock package.
package mock_gen

import (
    gomock "github.com/golang/mock/gomock"
    reflect "reflect"
)

// MockRepository is a mock of Repository interface
type MockRepository struct {
    ctrl     *gomock.Controller
    recorder *MockRepositoryMockRecorder
}

// MockRepositoryMockRecorder is the mock recorder for MockRepository
type MockRepositoryMockRecorder struct {
    mock *MockRepository
}

// NewMockRepository creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
    mock := &MockRepository{ctrl: ctrl}
    mock.recorder = &MockRepositoryMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
    return m.recorder
}

// Delete mocks base method
func (m *MockRepository) Delete() error {
    ret := m.ctrl.Call(m, "Delete")
    ret0, _ := ret[0].(error)
    return ret0
}

// Delete indicates an expected call of Delete
func (mr *MockRepositoryMockRecorder) Delete() *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete))
}

試す
テストでこんな感じに使えます

package gen

import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/smith-30/gopg/gen/mock_gen"
)

// Deleteが一回だけ呼ばれるか
// Returnに値をセットすれば振る舞いも変えられる
func TestSampleRepository_Delete(t *testing.T) {
    ctrl := gomock.NewController(t)
    mock := mock_gen.NewMockRepository(ctrl)

    mock.
        EXPECT().
        Delete().
        Return(nil).
        Times(1)

    mock.Delete()
    mock.Delete()
}

まとめ

こういうgenerate系は、後から定義を追加しても
追加したことによる副作用を開発者があまり考えなくて済むのがよいですよね
積極的に使っていきたい

smith-30
engineer elmをやりたいと思っている
elm-jp
主に日本で活動する Elm 利用者のコミュニティです。
https://elm-lang.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away