最近Goをキャッチアップしているので備忘録として残しておきます。
1package main
2
3import "fmt"
4
5func main() {
6 var money int
7 if true {
8 money := 100
9 fmt.Println(money)
10 } else {
11 money := 200
12 fmt.Println(money)
13 }
14 fmt.Println(money)
15}上記コードの出力は以下のようになります。
つまり、:= を使用すると外側の変数ではなく内側の変数に対し代入することになるので、外側の変数は初期値の0のままです。
1100
20もし外側の変数に代入したいのであれば以下のようにするとよさそうです。
:= ではなく = とすることでブロックの外側に宣言されているmoney変数に対し代入されるようになります。
1package main
2
3import "fmt"
4
5func main() {
6 var money int
7 if true {
8 money = 100
9 fmt.Println(money)
10 } else {
11 money = 200
12 fmt.Println(money)
13 }
14 fmt.Println(money)
15}1100
2100これはGoというよりプログラミング言語全般に言えることだと思いますが重要です。
以下のコードは結構ネストされていてぱっと見何をやっているかわからないと思います。。
1package main
2
3import "fmt"
4
5func f(money1, money2 int) int {
6 if money1 > money2 {
7 return money1
8 } else {
9 if money1 > 10000 {
10 return money1 + 10000
11 } else {
12 if money1 > 1000 {
13 return money1 + 1000
14 } else {
15 return 0
16 }
17 }
18 }
19}
20
21func main() {
22 fmt.Println(f(10000, 5000))
23}ネストをなくし、わかりやすくしたものが以下です。
1列目に条件分岐があり、2列目には返される値があり、それらがどのケースにも該当するので認知負荷が下がったと思います。
早期returnをガンガン使いelseを削っていくことで、コードはだいぶ読みやすくなります。
1package main
2
3import "fmt"
4
5func f(money1, money2 int) int {
6 if money1 > money2 {
7 return money1
8 }
9 if money1 > 10000 {
10 return money1 + 10000
11 }
12 if money1 > 1000 {
13 return money1 + 1000
14 }
15 return 0
16}
17
18func main() {
19 fmt.Println(f(10000, 5000))
20}init関数はアプリケーションの状態を初期化するために使われる関数であり、パッケージが初期化される時に実行されます。
他の言語で似たようなものだとPythonの__init__.pyかなと思います。
1.
2├── go.mod
3├── logic
4│ └── main.go
5└── main.go上記のような構成があり、それぞれのファイルは以下となっていたとします。
1// main.go
2package main
3
4import (
5 "fmt"
6
7 "github.com/example/logic/logic"
8)
9
10func init() {
11 fmt.Println("main package")
12}
13
14func main() {
15 fmt.Println(logic.Logic())
16}1// logic/main.go
2package logic
3
4import "fmt"
5
6func init() {
7 fmt.Println("logic package")
8}
9
10func Logic() string {
11 return "logic"
12}main.goではlogicパッケージをimportしているので、Goランタイムは最初にlogicパッケージのinit関数を実行し、次にmain.goのinit関数を実行し、最後にmain.goのmain関数を実行します。
なので出力はこうです。
1logic package
2main package
3logic仮にlogicパッケージに2つファイルがあり、そのどちらにもinit関数が実装されていた場合、ファイル名の辞書順で呼ばれます。
なので、init関数同士で依存関係を持たせたりするとかなりゴチャついて保守が大変になりそうです。
ちなみにですが、1つのファイルに複数のinit関数を実装することもできるようですが、あまり用途が見つからなかったので深追いはやめました。
また、init関数はエラーを返さないので、エラーを通知したければpanicを呼んでアプリケーション停止させる必要があります。
基本的に呼び出し側でエラーハンドリングしてアプリケーションを停止させるか、それともログを出して何かするのかを決めるのが方針としてはいいと思うので、init関数でエラー発生の可能性がある処理を行うのは微妙です。
また、init関数はテストがしにくいです。モック差し替えができないので、全く関係のないパッケージのテストでinit処理が実行される等の無駄が発生します。
そのためinit関数は以下のようなユースケースで使うのがいいかなと思いました。
インターフェースはオブジェクトの振る舞いを定義します。
Goの場合は明示的ではなく暗黙的にインタフェースが満たされます。
1package main
2
3import "fmt"
4
5type IMoneyCalculater interface {
6 calc(money int) int
7}
8
9type MoneyCalculater struct{}
10
11func (m MoneyCalculater) calc(money int) int {
12 return money + 100
13}
14
15func calc(c IMoneyCalculater, money int) int {
16 return c.calc(money)
17}
18
19func main() {
20 m := MoneyCalculater{}
21 r := calc(m, 100)
22 fmt.Println(r)
23}上記のコードのMoneyCalculaterは明示的にIMoneyCalculaterを参照していませんが、IMoneyCalculaterが持つcalcメソッドを実装することで、IMoneyCalculaterインターフェースとして暗黙的に満たされます。
calc関数ではIMoneyCalculaterが型として指定されていますが、MoneyCalculaterを問題なく渡すことができますね。例えば、MoneyCalculaterのcalcメソッドを削除したり、名前変更したり、引数の型を変えたり、返り値の型を変更するとコンパイルエラーになります。
しかしGoでは「具体的な必要性が生まれるまではインターフェースを使用するべきではない」という思想です。
というのも、インターフェースを過剰に使用してしまうと以下のような問題が発生するからです。
インタフェースが「どのように問題を解決し、よくしてくれるのか」という問いに対し明確な答えがない場合は、素直に実装するのが手段として適切です。
完璧な抽象化を推測するのはかなり難しいので、なんとなく将来的に抽象化しといた方がいいだろうのような曖昧な時は、インターフェースを避けた方がいいかなと思います。
レポジトリ層は抽象化してモックできるようにすれば、ユースケース層のユニットテストが簡単になる、のような時はガンガンインターフェースを使っても問題ないです。
意図も明確ですし、それくらいの抽象化ならコードの流れが複雑になることもなさそうなので許容範囲かなと。
昔Goをやった時にジェネリクスはなかった記憶でしたが、1.18より導入されたようです。
ジェネリクスは型による恩恵を受けながらも、柔軟に関数を作ったりができるのでとてもありがたい機能です。
ジェネリクスがないとこんな感じで型によって関数を作ったりしてました。
1package main
2
3import "fmt"
4
5// int用
6func ContainsInt(slice []int, v int) bool {
7 for _, s := range slice {
8 if s == v {
9 return true
10 }
11 }
12 return false
13}
14
15// string用
16func ContainsString(slice []string, v string) bool {
17 for _, s := range slice {
18 if s == v {
19 return true
20 }
21 }
22 return false
23}
24
25// float64用
26func ContainsFloat64(slice []float64, v float64) bool {
27 for _, s := range slice {
28 if s == v {
29 return true
30 }
31 }
32 return false
33}
34
35func main() {
36 ints := []int{1, 2, 3}
37 fmt.Println(ContainsInt(ints, 2)) // true
38
39 strs := []string{"a", "b", "c"}
40 fmt.Println(ContainsString(strs, "d")) // false
41
42 floats := []float64{1.1, 2.2, 3.3}
43 fmt.Println(ContainsFloat64(floats, 3.3)) // true
44}これはかなり冗長です。ただ、ジェネリクスを使うとかなりスッキリ書けます。
Contains関数の T が型パラメータであり、呼び出し時にintやstringに置き換えられます。Contains[int] や Contains[string] な感じで展開されるイメージですね。
comparable は組み込みの型制約の1つで「== と != が使える型だけ許可する」という意味です。
1package main
2
3import "fmt"
4
5func Contains[T comparable](slice []T, v T) bool {
6 for _, s := range slice {
7 if s == v {
8 return true
9 }
10 }
11 return false
12}
13
14func main() {
15 // intスライス
16 ints := []int{1, 2, 3}
17 fmt.Println(Contains(ints, 2)) // true
18 fmt.Println(Contains(ints, 5)) // false
19
20 // stringスライス
21 strs := []string{"go", "rust", "ts"}
22 fmt.Println(Contains(strs, "rust")) // true
23 fmt.Println(Contains(strs, "java")) // false
24
25 // 明示的に型を指定することも可能
26 fmt.Println(Contains[int]([]int{10, 20, 30}, 20)) // true
27}また、以下のような感じで特定の型しか受け入れできないように、型制約を定義することもできます。ちょっとTSのユニオン型にも似ています。
1package main
2
3import "fmt"
4
5type CustomType interface {
6 ~int | ~float32 | ~float64
7}
8
9func add[T CustomType](a T, b T) T {
10 return a + b
11}
12
13func main() {
14 // int
15 fmt.Println(add(3, 7)) // 10
16
17 // float32
18 var f1, f2 float32 = 1.5, 2.5
19 fmt.Println(add(f1, f2)) // 4
20
21 // float64
22 fmt.Println(add(1.1, 2.2)) // 3.3
23
24 // カスタム型(基底型が int)
25 type MyInt int
26 var x, y MyInt = 10, 20
27 fmt.Println(add(x, y)) // 30
28}こんな感じで、1つの関数で複数の型を扱いたい場面は多くあるので、Goにもジェネリクスが導入されて凄く助かります。