最近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では「具体的な必要性が生まれるまではインターフェースを使用するべきではない」という思想です。
というのも、インターフェースを過剰に使用してしまうと以下のような問題が発生するからです。
インタフェースが「どのように問題を解決し、よくしてくれるのか」という問いに対し明確な答えがない場合は、素直に実装するのが手段として適切です。
完璧な抽象化を推測するのはかなり難しいので、なんとなく将来的に抽象化しといた方がいいだろうのような曖昧な時は、インターフェースを避けた方がいいかなと思います。
レポジトリ層は抽象化してモックできるようにすれば、ユースケース層のユニットテストが簡単になる、のような時はガンガンインターフェースを使っても問題ないです。
意図も明確ですし、それくらいの抽象化ならコードの流れが複雑になることもなさそうなので許容範囲かなと。