private repositoryにあるGo Modulesをgo getする場合の設定
何をしたいか
プライベートリポジトリのモジュールを go get
したい
どうするか
パーソナルアクセストークンを発行して、go get
の際にトークンを用いてアクセスできるように git
の設定をします。
git config --global url."https://${access_token}:x-oauth-basic@github.com/<myorg>/".insteadOf "https://github.com/<myorg>/"
--glocal
に設定しないと go get
の際に参照ができないため注意が必要です。また設定する際は、複数のユーザーないしはOrganizationのプライベートリポジトリを参照する場合は <myorg>
のようにOrganization単位で config
しておくのが良いでしょう。上記を実行すると ~/.gitconfig
に設定が追加されたことが確認できます。
go-cmpを使う理由とTipsの紹介
はじめに
これは Go 4 Advent Calendar 2020 8日目の記事です。
Goのテストにおいて、構造体を含めて型の値を比較したいという場合は往々にしてあります。ロジックの結果はなんらかの値として作用することが多いですから、型の値を比較したい、というのは自然です。私は型の値が等価であるかどうか判定するために、go-cmp
というライブラリを使うことが多いです。
しかしGoにおける等価性の仕様は決まっていますし、標準ライブラリの reflect
パッケージにも DeepEqual
という deeply equal,
かどうか判定するメソッドがあります。そこで本記事ではなぜわざわざ go-cmp
を使っているのかという理由と、go-cmp
を使ったときにどのようにして使うか?という go-cmp
の使う上でよく使う以下のTipsを提供したいと思います。
- Tips
go-cmpを使う理由
私は大きく以下の3つの理由から go-cmp
を使うケースが多いです。
- 値にDiffがあった場合に見やすい
- 比較対象を柔軟に制御しやすい
- テストの書き方は標準的な書き方によせたい
基本的には go-cmp
を用いた比較は reflect.DeepEqual
の上記互換と考えています。reflect.DeepEqual
の比較方法にふれる前に、以下のような構造体の値を ==
の等価演算子を用いて比較するテストを考えてみます。
- main_test.go
package main import "testing" type User struct { UserID string UserName string } func TestTom(t *testing.T) { tom := User{ UserID: "0001", UserName: "Tom", } tom2 := User{ UserID: "0001", UserName: "Tom", } if tom != tom2 { t.Errorf("User tom is mismatch, tom=%v, tom2=%v", tom, tom2) } }
=== RUN TestTom --- PASS: TestTom (0.00s) PASS
https://play.golang.org/p/WIvR4SGFJ87
上記のテストはPASSします。==
の等価演算子を用いた比較については Comparison operators に記載があります。つまり構造体の値は、構造体のすべてのフィールドが比較可能(つまり map
型や slice
型, func
型の値を含んでいないということです)の場合に比較できます。構造体のブランクフィールドでないフィールドが等しい場合に等価とみなされます。
slice
型を ==
で比較するとどうなるでしょうか?構造体の中に slice
型を含んでいる値を ==
で比較した場合はコンパイルエラーになります。
https://play.golang.org/p/Wn2MMNAxoTT
上記のGo Playgroundの例にあるような slice
型を含む場合や map
型の値、ポインタが参照する値などを比較したい場面は多くあります。その場合に標準ライブラリの reflect.DeepEqual
を用いると deeply equal,
に判定することができます。slice
型の場合に限れば、スライスの要素の値が deeply equal,
であれば等しいとされます。[]string
であれば、string
の slice
の値が要素の位置まで含めて同じ場合に等価と判定でき、多くの場面でこの deeply equal,
の等価判定が役に立ちます。
つまり reflect.DeepEqual
を使った以下のテストはPASSします。
func TestTom(t *testing.T) { tom := User{ UserID: "0001", UserName: "Tom", Languages: []string{"Java", "Go"}, } tom2 := User{ UserID: "0001", UserName: "Tom", Languages: []string{"Java", "Go"}, } if !reflect.DeepEqual(tom, tom2) { t.Errorf("User tom is mismatch, tom=%v, tom2=%v", tom, tom2) } }
=== RUN TestSlice --- PASS: TestSlice (0.00s) PASS
https://play.golang.org/p/HnX_HO-s9Y3
さて、go-cmp
を使った場合ですが、reflect.DeepEqual
を使った場合と同様にテストはPASSします。
func TestTom(t *testing.T) { // ... if diff := cmp.Diff(tom, tom2); diff != "" { t.Errorf("User value is mismatch (-tom +tom2):\n%s", diff) } }
=== RUN TestTom --- PASS: TestTom (0.00s) PASS
https://play.golang.org/p/whGERlULOXt
- 値にDiffがあった場合に見やすい
go-cmp
を使う理由の1つとして値にDiffがあった場合に reflect.DeepEqual
よりもDiffがわかりやすい(個人差あり)ということがあります。仮にスライスの値に差があった場合を考えてみます。
reflect.DeepEqual
の場合
package main import ( "reflect" "testing" ) type User struct { UserID string UserName string Languages []string } func TestTom(t *testing.T) { tom := User{ UserID: "0001", UserName: "Tom", Languages: []string{"Java", "Go"}, } tom2 := User{ UserID: "0001", UserName: "Tom", Languages: []string{"Ruby", "Go"}, } if !reflect.DeepEqual(tom, tom2) { t.Errorf("User tom is mismatch, tom=%v, tom2=%v", tom, tom2) } }
Diffの表示は以下のようになります。つまり開発者が t.Errorf
で表示している文字列です。
=== RUN TestTom prog.go:28: User tom is mismatch, tom={0001 Tom [Java Go]}, tom2={0001 Tom [Ruby Go]} --- FAIL: TestTom (0.00s) FAIL
https://play.golang.org/p/yEg8hLy_bhY
go-cmp
を使った場合は以下のようになります。
package main import ( "testing" "github.com/google/go-cmp/cmp" ) type User struct { UserID string UserName string Languages []string } func TestTom(t *testing.T) { // ... if diff := cmp.Diff(tom, tom2); diff != "" { t.Errorf("User value is mismatch (-tom +tom2):\n%s", diff) } }
以下のように構造体の値でDiffがあったもののみが -
と +
で表示されます、Diffがあった値が直感的に分かるようになっています。構造体のフィールドが複雑になるほど、Diffが見やすいことが役に立ちます。
=== RUN TestTom prog.go:29: User value is mismatch (-tom +tom2): main.User{ UserID: "0001", UserName: "Tom", Languages: []string{ - "Java", + "Ruby", "Go", }, } --- FAIL: TestTom (0.00s) FAIL
https://play.golang.org/p/58FuMU6IUMm
また nil
スライスと空スライスの違いがあった場合に reflect.DeepEqual
では異なったものとしてみなされますが、出力する文字列だけではわかりにくい時もあります。
go-cmp
だと以下のように出力されるため nil
スライスと空スライスで値に差がある場合もわかりやすいです。
- Languages: []string{}, + Languages: nil,
- 比較対象を柔軟に制御しやすい
構造体の中で、あるフィールドは比較対象から除きたい場合などがあります。このような場合に go-cmp
のオプションを用いることで比較対象から簡単に除くことができます。この方法については以下の go-cmp
のTips の項目の中で紹介します。
- テストの書き方は標準的な書き方によせたい
GoのFAQ(Where is my favorite helper function for testing?)にもあるようにGoはテストのフレームワークを提供していません。とはいえ、一方でやはりチームによっては stretchr/testify などのフレームワークを使ったほうが生産性が上がる、などの意見もあります。どのようなライブラリを使うかはプロジェクトメンバの構成や状況によって変わるので一概に何が良い、と決めることはできません。
それでも私個人としてはテストはなるべく標準的な書き方によせていきたい、と考えています。go-cmp
は reflect.DeeqEqual
の代用程度として使うことができるため、全体的な書き方は標準的な書き方のままで大丈夫、という点も go-cmp
を使う理由の一つです。
go-cmpのTips
前置きが長くなりましたが、go-cmp
を使ったときのTipsを紹介します。なお go-cmp
のバージョンは2020/12/08現在最新の v0.5.4
を使っています。
unexportedなフィールドが存在する場合に比較する
go-cmp
のドキュメントにもあるように go-cmp
は reflect.DeepEqual
とは異なり、デフォルトではunexportedなフィールドは比較対象になりません。というかオプションを何も指定しないとドキュメントに記載されているように panic
になります。
package main import ( "testing" "github.com/google/go-cmp/cmp" ) func TestX(t *testing.T) { type X struct { numUnExport int NumExport int } num1 := X{100, -1} num2 := X{999, -1} if diff := cmp.Diff(num1, num2); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
=== RUN TestX --- FAIL: TestX (0.00s) panic: cannot handle unexported field at {main.X}.numUnExport: "main".X consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported [recovered] panic: cannot handle unexported field at {main.X}.numUnExport: "main".X consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported goroutine 18 [running]: testing.tRunner.func1.1(0x566380, 0xc00010a2f0) /usr/local/go-faketime/src/testing/testing.go:1072 +0x30d testing.tRunner.func1(0xc000106300) /usr/local/go-faketime/src/testing/testing.go:1075 +0x41a panic(0x566380, 0xc00010a2f0) /usr/local/go-faketime/src/runtime/panic.go:969 +0x1b9 github.com/google/go-cmp/cmp.validator.apply(0xc000144280, 0x565c80, 0xc0001001d0, 0xa2, 0x565c80, 0xc0001001e0, 0xa2) /tmp/gopath381553566/pkg/mod/github.com/google/go-cmp@v0.5.4/cmp/options.go:244 +0x68f github.com/google/go-cmp/cmp.(*state).tryOptions(0xc000144280, 0x5cb000, 0x565c80, 0x565c80, 0xc0001001d0, 0xa2, 0x565c80, 0xc0001001e0, 0xa2, 0xca20) /tmp/gopath381553566/pkg/mod/github.com/google/go-cmp@v0.5.4/cmp/compare.go:303 +0x132 github.com/google/go-cmp/cmp.(*state).compareAny(0xc000144280, 0x5c9aa0, 0xc00017c000) /tmp/gopath381553566/pkg/mod/github.com/google/go-cmp@v0.5.4/cmp/compare.go:258 +0x2ef github.com/google/go-cmp/cmp.(*state).compareStruct(0xc000144280, 0x5cb000, 0x5788a0, 0x5788a0, 0xc0001001d0, 0x99, 0x5788a0, 0xc0001001e0, 0x99) /tmp/gopath381553566/pkg/mod/github.com/google/go-cmp@v0.5.4/cmp/compare.go:427 +0x685 github.com/google/go-cmp/cmp.(*state).compareAny(0xc000144280, 0x5c9920, 0xc000108680) /tmp/gopath381553566/pkg/mod/github.com/google/go-cmp@v0.5.4/cmp/compare.go:286 +0xe35 github.com/google/go-cmp/cmp.Diff(0x5788a0, 0xc0001001d0, 0x5788a0, 0xc0001001e0, 0x0, 0x0, 0x0, 0x4f44e4, 0x65ea30) /tmp/gopath381553566/pkg/mod/github.com/google/go-cmp@v0.5.4/cmp/compare.go:119 +0xab main.TestX(0xc000106300) /tmp/sandbox897774203/prog.go:19 +0xc5 testing.tRunner(0xc000106300, 0x5a0348) /usr/local/go-faketime/src/testing/testing.go:1123 +0xef created by testing.(*T).Run /usr/local/go-faketime/src/testing/testing.go:1168 +0x2b3
https://play.golang.org/p/15Yu_aD4ahO
unexportedなフィールドをテストの比較対象にするときは cmp.AllowUnexported
を用います。差分があった場合はDiffが表示されます。
package main import ( "testing" "github.com/google/go-cmp/cmp" ) func TestX(t *testing.T) { type X struct { numUnExport int NumExport int } num1 := X{100, -1} num2 := X{999, -1} opt := cmp.AllowUnexported(X{}) if diff := cmp.Diff(num1, num2, opt); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
=== RUN TestX prog.go:22: X value is mismatch (-num1 +num2): main.X{ - numUnExport: 100, + numUnExport: 999, NumExport: -1, } --- FAIL: TestX (0.00s) FAIL
https://play.golang.org/p/L9P8p7ASLyo
unexportedなフィールドが存在する場合に無視する
unexportedなフィールドを比較対象から除くときは cmpopts.IgnoreUnexported
を使います。
package main import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) func TestX(t *testing.T) { type X struct { numUnExport int NumExport int } num1 := X{100, -1} num2 := X{999, -1} opt := cmpopts.IgnoreUnexported(X{}) if diff := cmp.Diff(num1, num2, opt); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
比較対象から除外されて、テストがPASSするようになったことが分かります。
=== RUN TestX --- PASS: TestX (0.00s) PASS
https://play.golang.org/p/Rfh4aVjHGNt
構造体のあるフィールドを比較対象から除く
時刻など構造体のうち一部のフィールドは比較対象から除きたい場合があります。一部のフィールドを比較対象から除外したい場合は cmpopts.IgnoreFields
を使います。除外したいフィールドを文字列で列挙することで比較対象から除くことができます。
以下の場合は X
構造体の CreateAt
と UpdateAt
の時刻に関するフィールドを比較対象から除外しています。
func TestX(t *testing.T) { type X struct { NumExport int CreateAt time.Time UpdateAt time.Time } num1 := X{-1, time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), time.Now()} num2 := X{-1, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Now()} opt := cmpopts.IgnoreFields(X{}, "CreateAt", "UpdateAt") if diff := cmp.Diff(num1, num2, opt); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
=== RUN TestX --- PASS: TestX (0.00s) PASS
https://play.golang.org/p/9Geaky978t_J
構造体の中に構造体のスライスが含まれていて、そのスライスに含まれる構造体の一部のフィールドを比較対象から除外することも cmpopts.IgnoreFields
を指定することで除外可能です。
func TestX(t *testing.T) { type Item struct { HashKey string CreatedAt time.Time } type X struct { Items []Item } x1 := X{[]Item{ {HashKey: "aaa", CreatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)}, {HashKey: "bbb", CreatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)}, }} x2 := X{[]Item{ {HashKey: "aaa", CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, {HashKey: "bbb", CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, }} opt := cmpopts.IgnoreFields(Item{}, "CreatedAt") if diff := cmp.Diff(x1, x2, opt); diff != "" { t.Errorf("X value is mismatch (-x1 +x2):%s\n", diff) } }
=== RUN TestX --- PASS: TestX (0.00s) PASS
https://play.golang.org/p/KCtP21m6ioc
複数のオプションを指定する
複数の条件を cmp.Diff
のオプションに渡したい場合があります。例えば
- unexportedなフィールドを比較対象から除外
- 構造体の時刻に関するフィールドを比較対象から除外
といった上記の両方をオプションとして指定したい場合です。このような場合は cmp.Diff
のオプションに cmp.Option
のスライスを渡すことで複数の条件をオプションとして指定できます。
func TestX(t *testing.T) { type X struct { NumExport int numUnExport int CreateAt time.Time UpdateAt time.Time } num1 := X{100, -1, time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), time.Now()} num2 := X{999, -111, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Now()} opts := []cmp.Option{ cmpopts.IgnoreUnexported(X{}), cmpopts.IgnoreFields(X{}, "CreateAt", "UpdateAt"), } if diff := cmp.Diff(num1, num2, opts...); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
時刻に関する項目の差分は比較対象から無視され、またunxeportedなフィールドも無視されていることがわかります。
=== RUN TestX prog.go:29: X value is mismatch (-num1 +num2): main.X{ - NumExport: 100, + NumExport: 999, ... // 3 ignored fields } --- FAIL: TestX (0.00s) FAIL
https://play.golang.org/p/uGFUSLEFwZF
スライスの要素の順序を無視する
構造体の中にスライスを含む場合に、スライスに含まれる値が一致していることを確認し、順序までは一致していなくてもOKとする場合です。通常はスライスの順序まで一致していることが求められます。
func TestX(t *testing.T) { type X struct { Numbers []int } num1 := X{[]int{1, 2, 3, 4, 5}} num2 := X{[]int{5, 4, 3, 2, 1}} if diff := cmp.Diff(num1, num2); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
スライスの順序が一致していない場合はDiffとして差分があるものとして検知されます。
=== RUN TestX prog.go:19: X value is mismatch (-num1 +num2): main.X{ Numbers: []int{ - 1, 2, 3, 4, 5, + 5, 4, 3, 2, 1, }, } --- FAIL: TestX (0.00s) FAIL
多くの場合は順序も含めて一致することを確認するとして問題ないですが、一部の状況ではスライスの中身が一致していればOKとみなしたい場合があるでしょう。その場合はスライスの中身をソートするようなオプションを指定するとうまくいきます。
func TestX(t *testing.T) { type X struct { Numbers []int } num1 := X{[]int{1, 2, 3, 4, 5}} num2 := X{[]int{5, 4, 3, 2, 1}} opt := cmpopts.SortSlices(func(i, j int) bool { return i < j }) if diff := cmp.Diff(num1, num2, opt); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
=== RUN TestX --- PASS: TestX (0.00s) PASS
https://play.golang.org/p/hLrpfqiSlii
ソートの条件は任意に実装できるため、例えば独自の構造体のスライスの順序をソートすることも可能です。例えば
type X struct { HashKey string SortKey string }
という構造体のスライスについての順序をソートするには、以下のような実装例が考えられます。
func TestX(t *testing.T) { x1 := []X{ {HashKey: "AAA", SortKey: "AAA"}, {HashKey: "BBB", SortKey: "BBB"}, {HashKey: "BBB", SortKey: "CCC"}, } x2 := []X{ {HashKey: "BBB", SortKey: "CCC"}, {HashKey: "AAA", SortKey: "AAA"}, {HashKey: "BBB", SortKey: "BBB"}, } opt := cmpopts.SortSlices(func(i, j X) bool { // ハッシュキーとソートキーの昇順でソート return i.HashKey < j.HashKey || (i.HashKey == j.HashKey && i.SortKey < j.SortKey) }) if diff := cmp.Diff(x1, x2, opt); diff != "" { t.Errorf("X value is mismatch (-x1 +x2):%s\n", diff) } }
=== RUN Test --- PASS: Test (0.00s) PASS
https://play.golang.org/p/8uUD5XKLYXs
空スライスとnilスライスの違いを無視する
テスト上は空スライスとnilスライスの違いを明確にしてテストすることで十分な場合がありますが、cmpopts.EquateEmpty()
をオプションに用いることで無視することもできます。
func TestX(t *testing.T) { type X struct { Numbers []int } num1 := X{[]int{}} num2 := X{nil} opt := cmpopts.EquateEmpty() if diff := cmp.Diff(num1, num2, opt); diff != "" { t.Errorf("X value is mismatch (-num1 +num2):%s\n", diff) } }
=== RUN TestX --- PASS: TestX (0.00s) PASS
https://play.golang.org/p/jdchcmmR5DF
その他
その他 float64
型の差分を加味して、一定の差分以内であれば等価とみなすような cmpopts.EquateApprox
や math.NaN()
と math.NaN()
の比較を等価とみなせるような cmpopts.EquateNaNs()
、time
に関する一定の差分を等価とみなすような cmpopts.EquateApproxTime
など様々なオプションがあります。
本記事に記載したものに比べるとユースケースは少ないかもしれませんが、様々なオプションが指定できるようになっています。詳しくは Documentation や util_test.go
の単体テストケースに記載されている内容を見るとよいでしょう。
まとめ
go-cmp
の使う理由やよく使うオプションのTipsを紹介しました。go-cmp
は reflect.DeppEqual
の代替として使うことができ、テストの書き方自体は標準の書き方を使うことができます。テスト結果のDiffが見やすくなるだけでも生産性は上がると思いますので、まだ使ったことがない方は導入を検討してみてはいかがでしょうか。
条件付きのロジックをTempleteMethodパターンで置換する
コードが複雑になる要因の一つとしてif文やswitch文による条件付きロジックがあります。以下のような給与を計算する実装例を考えてみます。(employeeType
が文字列、という話はおいておきます)
switch
文があると呼び出し側の実装が複雑になります。switch
文を呼び出し元の実装からリファクタリングします。
- main.go
package main import "fmt" func main() { employeeType := "engineer" var amount int switch employeeType { case "engineer": amount = 1 case "salesman": amount = 10 default: amount = -1 } fmt.Println("amount", amount) }
TemplateMethodパターン
条件付きのロジックをSteragyパターンで置換する の記事ではStrategyパターンを使って呼び出し側の構造体にアルゴリズムを実装することができました。今回はTemplateMethodパターンで実装します。
Strategyパターンと本質的には同様で、TemplateMethodパターンでは抽象クラスやインターフェースを使って実装します。呼び出し元の main.go
はStrategyパターンと変更はありません。
- model.go
package main type Employee interface { payAmount() int } func NewEmployee(employeeType string) Employee { switch employeeType { case "engineer": return Engineer{} case "salesman": return Salesman{} default: return dummy{} } } type Engineer struct{} func (e Engineer) payAmount() int { return 1 } type Salesman struct{} func (s Salesman) payAmount() int { return 10 } type dummy struct{} func (d dummy) payAmount() int { return -1 }
- main.go
package main import "fmt" func main() { e := NewEmployee("engineer") fmt.Println("amount", e.payAmount()) }
- 出力結果
$ go run . amount 1
呼び出し元からは Employee
インターフェースのメソッドが呼び出されます。実装の詳細は Employee
インターフェースを実装している各構造体のメソッドです。本例では Engineer
構造体のメソッドとなります。
条件付きのロジックをStrategyパターンで置換する
コードが複雑になる要因の一つとしてif文やswitch文による条件付きロジックがあります。以下のような給与を計算する実装例を考えてみます。(employeeType
が文字列、という話はおいておきます)
switch
文があると呼び出し側の実装が複雑になります。switch
文を呼び出し元の実装からリファクタリングします。
- main.go
package main import "fmt" func main() { employeeType := "engineer" var amount int switch employeeType { case "engineer": amount = 1 case "salesman": amount = 10 default: amount = -1 } fmt.Println("amount", amount) }
Strategyパターン
呼び出し側のロジックを、適切なクラスのメソッドとして実装して、呼び出し側の実装をシンプルにします。デザインパターンのStrategyパターンを導入して、strcut
のメソッドとしてアルゴリズムを実装します。Strategyパターンを導入するとアルゴリズムを任意に差し替えることができます。employeeType
が追加になった場合には、呼び出し元の実装には変更は不要です。変更の範囲を局所化できます。今回の場合は employeeType
が engineer
と salesman
の2種類の計算ロジックがあれば十分です。
実装上のポイントは、アルゴリズムを実装した構造体のメソッドに移譲して呼び出すということです。NewEmployee
の値を例えばJavaのSpringを使っている場合はDIコンテナから取得する、といったことも可能です。Goの場合はDIコンテナはないので諦めましょう。*1
- model.go
package main func NewEmployee(employeeType string) Employee { switch employeeType { case "engineer": return Employee{strategy: EngineerStrategy{}} case "salesman": return Employee{strategy: SalesmanStrategy{}} default: return Employee{} } } type Employee struct { strategy EmployeeStrategy } func (e Employee) payAmount() int { if e.strategy == nil { return -1 } return e.strategy.payAmount() } type EmployeeStrategy interface { payAmount() int } type EngineerStrategy struct{} func (es EngineerStrategy) payAmount() int { return 1 } type SalesmanStrategy struct{} func (ss SalesmanStrategy) payAmount() int { return 10 }
- main.go
package main import "fmt" func main() { e := NewEmployee("engineer") fmt.Println("amount", e.payAmount()) }
- 出力結果
$ go run . amount 1
Employee
構造体は呼び出し元から呼び出されますが、アルゴリズムの詳細は Employee
構造体のフィールドに含まれる EmployeeStrategy
インターフェースの payAmount
メソッドに移譲しています。main.go
ではインターフェースのメソッドを呼び出しています。NewEmployee
のコンストラクタの引数によって振る舞いを差し替えることができました。
*1:google/wireは必要な依存関係のコードを生成してくれますが、SpringのDIコンテナのように実行時にインスタンスを取得するものではありません
IDのモックを作る
IDを生成する必要があるときにUUIDやxidを用いることがあります。テストの結果にIDが含まれてしまう場合にIDの生成をモックする方法を紹介します。本記事では rs/xid を用いてIDを生成することにします。ポイントは関数型の変数にすることです。もちろんインターフェースのメソッドすることでモックすることもできます。
- a.go
package a import ( "time" "github.com/rs/xid" ) var generateID = generateIDFunc func generateIDFunc(t time.Time) string { return xid.NewWithTime(t).String() }
- a_test.go
package a import ( "testing" "time" ) func TestGenerateID(t *testing.T) { generateID = func(t time.Time) string { return "test-id" } got := generateID(time.Now()) want := "test-id" if got != want { t.Errorf("wrong id, got=%v, want=%v", got, want) } }
$ go test -v === RUN TestGenerateID --- PASS: TestGenerateID (0.00s) PASS ok github.com/d-tsuji/go-sandbox/a 0.177s
簡単ですが、このようにしてランダムな値をモックすることができます。
Goで構造体をDeepCopyする
はじめに
Goで構造体のDeepCopyする方法の紹介です。Goのsliceやmapは値への参照を保持しているため、単純に値をコピーするだけではDeepCopyできないことはよく知られています。以下は間違った実装例です。
package main import "fmt" func main() { s := []int{1, 2, 3, 4} t := s fmt.Printf("[s] address: %p, values: %v\n", s, s) fmt.Printf("[t] address: %p, values: %v\n", t, t) // Output: //[s] address: 0xc00009e140, values: [1 2 3 4] //[t] address: 0xc00009e140, values: [1 2 3 4] // 参照先のアドレスの値を変更 s[0] = 999 fmt.Printf("[s] address: %p, values: %v\n", s, s) fmt.Printf("[t] address: %p, values: %v\n", t, t) // Output: //[s] address: 0xc00009e140, values: [999 2 3 4] //[t] address: 0xc00009e140, values: [999 2 3 4] }
sliceのDeepCopyは Go Slices: usage and internals にあるように組み込み関数である copy
を用いるのが正しい実装になります。(アドレスは実行環境によって変わります)
package main import "fmt" func main() { s := []int{1, 2, 3, 4} t := make([]int, len(s)) copy(t, s) fmt.Printf("[s] address: %p, values: %v\n", s, s) fmt.Printf("[t] address: %p, values: %v\n", t, t) // Output: // [s] address: 0xc00000c3c0, values: [1 2 3 4] // [t] address: 0xc00000c3e0, values: [1 2 3 4] // 参照先のアドレスの値を変更 s[0] = 999 fmt.Printf("[s] address: %p, values: %v\n", s, s) fmt.Printf("[t] address: %p, values: %v\n", t, t) // Output: // [s] address: 0xc00000c3c0, values: [999 2 3 4] // [t] address: 0xc00000c3e0, values: [1 2 3 4] }
構造体のDeepCopy
リフレクションを用いてGoでDeepCopyするライブラリは以下のようなライブラリがあります。
リフレクションを用いたDeepCopyは本記事では立ち入りませんので、紹介だけにとどめておきます。
go generateによるメソッド生成
本記事では go generate によって構造体のメソッドを生成してDeepCopyする方法を紹介します。例として以下のような myStruct をDeepCopyすることにします。
- mystruct.go
type myStruct struct { A int B string C []string D map[string]string E map[string][]string }
以下のライブラリを用いてDeepCopyするメソッドを生成します。
ライブラリをインストールします。
$ go get github.com/globusdigital/deep-copy
メソッドを生成します。
$ deep-copy -o mystruct_deepcopy.go --type myStruct .
するとコマンドで指定したファイルが生成されます。メソッドの実装は、もとの構造体の値をまずはそのままコピーしています。その後、sliceやmapのフィールドの値に関しては、make をして新しいメモリアドレスを確保し、組み込みのcopy関数を用いてDeepCopyしています。
- mystruct_deepcopy.go
// generated by deep-copy -o mystruct_deepcopy.go --type myStruct .; DO NOT EDIT. package main // DeepCopy generates a deep copy of myStruct func (o myStruct) DeepCopy() myStruct { var cp myStruct = o if o.C != nil { cp.C = make([]string, len(o.C)) copy(cp.C, o.C) } if o.D != nil { cp.D = make(map[string]string, len(o.D)) for k, v := range o.D { cp.D[k] = v } } if o.E != nil { cp.E = make(map[string][]string, len(o.E)) for k, v := range o.E { var cpv []string if v != nil { cpv = make([]string, len(v)) copy(cpv, v) } cp.E[k] = cpv } } return cp }
テストして構造体を比較します。テストには google/go-cmp を利用します。
- mystruct_test.go
package main import ( "testing" "github.com/google/go-cmp/cmp" ) func Test_newMyStruct(t *testing.T) { src := &myStruct{ A: 1, B: "test", C: []string{"A", "B", "C"}, D: map[string]string{ "A": "Apple", }, E: map[string][]string{ "fruits": {"Apple", "Banana"}, }, } tar := src.DeepCopy() if diff := cmp.Diff(*src, tar); diff != "" { t.Errorf("mystruct mismatch (-want +got):\n%s", diff) } src.A = 999 src.B = "XXXXX" src.C[0] = "XXXXX" src.D["A"] = "XXXXX" src.E["fruits"] = []string{"XXXXX"} diff := cmp.Diff(*src, tar) if diff == "" { t.Errorf("expect different mystruct") } t.Logf("mystruct different (-src +tar):\n%s", diff) expect := myStruct{ A: 1, B: "test", C: []string{"A", "B", "C"}, D: map[string]string{ "A": "Apple", }, E: map[string][]string{ "fruits": {"Apple", "Banana"}, }, } if diff := cmp.Diff(expect, tar); diff != "" { t.Errorf("mystruct mismatch (-want +got):\n%s", diff) } }
以下のように想定どおりDeepCopyされていることが確認できました。
$ go test -v === RUN Test_newMyStruct Test_newMyStruct: mystruct_test.go:37: mystruct different (-src +tar): main.myStruct{ - A: 999, + A: 1, - B: "XXXXX", + B: "test", C: []string{ - "XXXXX", + "A", "B", "C", }, - D: map[string]string{"A": "XXXXX"}, + D: map[string]string{"A": "Apple"}, E: map[string][]string{ "fruits": { - "XXXXX", + "Apple", + "Banana", }, }, } --- PASS: Test_newMyStruct (0.00s) PASS ok github.com/d-tsuji/go-sandbox/s 0.190s
まとめ
globusdigital/deep-copy
のライブラリを用いて構造体にDeepCopy可能なメソッドを生成することで、構造体のDeepCopyする方法を紹介しました。go generateを用いてメソッドを生成する方法は Generating code で紹介されている stringer
をgo generateするアプローチと同様です。リフレクションを用いて動的にDeepCopyする方法よりもGoらしく感じます。
DynamoDB LocalとGoを用いた実装と、CircleCIでのCIの設定方法
本記事ではDynanmoDB Localを用いてローカルでのGoを用いた開発をする方法と、CircleCIを用いてCIを実施する方法を示します。
DynamoDB Localのセットアップ
まずDynamoDBにはDynamoDB Localと言われるエミュレーターがあるので、ローカルでの開発はDynamoDB Localを利用しましょう。
aws_access_key_id
や aws_secret_access_key
はダミーでOKです。何かしらの値は設定する必要があります。
# AWS profile(初回のみ) aws configure set aws_access_key_id dummy --profile local aws configure set aws_secret_access_key dummy --profile local aws configure set region ap-northeast-1 --profile local # DynamoDB Localの初回セットアップ docker run -d --name dynamodb -p 8000:8000 amazon/dynamodb-local:1.13.1 -jar DynamoDBLocal.jar -sharedDb
- 停止するとき
docker stop dynamodb
ローカルにコンテナが起動したことがわかります。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 76d708f8199c amazon/dynamodb-local:1.13.1 "java -jar DynamoDBL…" 3 hours ago Up 3 hours 0.0.0.0:8000->8000/tcp dynamodb
ユーザの情報を管理する user
テーブルを用いることにします。DynamoDB Localを用いるときは、prefixに local
を付与します。テーブル定義はこんな感じ。
{ "TableName": "local_user", "KeySchema": [ { "AttributeName": "user_id", "KeyType": "HASH" } ], "AttributeDefinitions": [ { "AttributeName": "user_id", "AttributeType": "S" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 } }
以下のようなアイテムを投入することにします。
{ "user_id": { "S": "001" }, "user_name": { "S": "gopher_1" } }
DynamoDB Localを用いたGoのテストの実装
Goの実装です。local_user
テーブルは環境変数で定義しておきます。DynamoDB Localを使わない場合は user
などというテーブル名を環境変数に定義します。
- db.go
import ( "log" "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" ) var ( db *dynamodb.DynamoDB userTable string ) func init() { dbEndpoint := os.Getenv("DYNAMO_ENDPOINT") region := os.Getenv("AWS_REGION") userTable = os.Getenv("DYNAMO_TABLE_USER") if userTable == "" { log.Fatal(`env variable "DYNAMO_TABLE_USER" is required`) } sess := session.Must(session.NewSession(&aws.Config{ Endpoint: aws.String(dbEndpoint), Region: aws.String(region), })) db = dynamodb.New(sess) }
DynamoDBからユーザ情報をフェッチするコードはこんな感じです。dynamodbav
というタグをstructのフィールドに付与することで、DynamoDBのキーを指定して構造体とマッピングすることができます。
- user.go
import ( "context" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" ) type User struct { UserID string `dynamodbav:"user_id"` UserName string `dynamodbav:"user_name"` } func FetchUserByID(ctx context.Context, userID string) (*User, error) { q := &dynamodb.GetItemInput{ TableName: aws.String(userTable), Key: map[string]*dynamodb.AttributeValue{ "user_id": { S: aws.String(userID), }, }, ConsistentRead: aws.Bool(true), } out, err := db.GetItemWithContext(ctx, q) if err != nil { return nil, fmt.Errorf("dynamodb fetch user: %w", err) } var user *User if err := dynamodbattribute.UnmarshalMap(out.Item, &user); err != nil { return nil, fmt.Errorf("dynamodb unmarshal: %w", err) } return user, nil }
上記のコードをテストして確認してみましょう。DynamoDB Localにテーブルを作成するコマンドやアイテムを投入するコマンドはexec.Command
を用いてAWS CLIコマンドをGoから実行することにします。なのでローカルの開発マシンからAWS CLIが実行できる必要があります。
- user_test.go
import ( "context" "os/exec" "reflect" "strings" "testing" ) func setupDB(t *testing.T) { cmds := []string{ `aws dynamodb --endpoint-url http://localhost:8000 create-table --cli-input-json file://./testdata/local_user.json`, `aws dynamodb put-item --endpoint-url http://localhost:8000 --table-name local_user --item file://./testdata/input_user.json`, } for _, cmd := range cmds { args := strings.Split(cmd, " ") if err := exec.Command(args[0], args[1:]...).Run(); err != nil { t.Logf("setup DynamoDB %v %s", err, cmd) } } } func teardownDB(t *testing.T) { cmds := []string{ `aws dynamodb --endpoint-url http://localhost:8000 delete-table --table local_user`, } for _, cmd := range cmds { args := strings.Split(cmd, " ") if err := exec.Command(args[0], args[1:]...).Run(); err != nil { t.Logf("teardown DynamoDB %v %s", err, cmd) } } } func TestFetchUserByID(t *testing.T) { setupDB(t) t.Cleanup(func() { teardownDB(t) }) tests := []struct { name string userID string want *User wantErr bool }{ { name: "normal", userID: "001", want: &User{ UserID: "001", UserName: "gopher_1", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := FetchUserByID(context.TODO(), tt.userID) if (err != nil) != tt.wantErr { t.Errorf("FetchUserByID() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("FetchUserByID() got = %v, want %v", got, tt.want) } }) } }
さあ、テストを実行してみます。
go test -v === RUN TestFetchUserByID === RUN TestFetchUserByID/normal --- PASS: TestFetchUserByID (6.01s) --- PASS: TestFetchUserByID/normal (0.01s) PASS ok github.com/d-tsuji/sample-circleci 6.231s
すばらしいですね。DynamoDB Localを用いてテストすることができました。
Circle CIの設定
天下り的に config.yml
を示します。この設定でCircleCI上でDynamoDB Localを用いたCIを実施することができます。
version: 2 jobs: test: docker: # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/ - image: circleci/golang:1.14.4 - image: amazon/dynamodb-local working_directory: /go/src/github.com/d-tsuji/sample-circleci # Environment values for all container environment: - GO111MODULE: "on" steps: - checkout - run: name: Install AWS CLI command: | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install - run: name: Fetch dependencies command: go mod download - run: name: Wait for DynamoDB command: | for i in `seq 1 10`; do nc -z localhost 8000 && echo Success && exit 0 echo -n . sleep 1 done echo Failed waiting for DyanmoDB Local && exit 1 - run: name: Run all unit tests command: | export AWS_REGION=ap-northeast-1 export AWS_ACCESS_KEY_ID=dummy export AWS_SECRET_ACCESS_KEY=dummy export DYNAMO_ENDPOINT=http://localhost:8000 export DYNAMO_TABLE_USER=local_user make test workflows: version: 2 build-and-test: jobs: - test
サマリ
DynamoDB Localを用いたGoの実装と、CircleCIの設定を示しました。本記事で扱ったコードは以下のリポジトリにコミットしてあります。
https://github.com/d-tsuji/sample-circleci
よいDynamoDB Localライフを。