転職先でGoをやるって聞いてたので有給消化中に『初めてのGo言語』を読んでいた。
けどそれよりも先にA Tour of Goをやっとくのが正解だった*1。書籍自体はGo言語の仕様について詳しく書かれていてめっちゃいい本なんだけど、100%僕の頭と書籍の使い方が悪かった。
「Goの基本完全に理解した」状態で『初めてのGo言語』はもう一回読むことにする。それはそれでより理解が深まりそう。

左にチュートリアル、右にコーディングエディタがあるUIになっている。トピックスごとにベースのコードを用意してくれていて、ガチャガチャ手元で書き換えて、フォーマットして、実行して結果を見るってことができる。途中練習問題も挟んでくれたりする。
A Tour of Goに限らず、僕自身が疑問に思って調べてたこととか試してたことも含まれる。
Naked Return は使うな
一見便利やーんと思うけど、長くなって最後にreturnだけされると戻り値が多かったりすると常に脳内メモリの保持してる値を記憶しないといけないから辛いのか。
Goのswitch
package main import ( "fmt" "runtime" ) func main() { fmt.Print("Go runs on ") switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: fmt.Printf("%s.\n", os) } }
go fmtでコード整形してもこれ。Kotlinとかの世界からくると若干違和感あって、caseのインデントが下がって欲しい気がしなくもない。Javaとかにあるフォールスルーがないのはめっちゃ快適。
Defer, Panic, and Recover
package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
このときの出力は以下の通り。
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.
Goにおけるポインタ
Goにおける *(ポインタ)は、「この関数は呼び出し元の値を変更するつもりがある」という意図を明示的に表現する仕組み と理解。
type User struct { Name string } func renameValue(u User) { u.Name = "Bob" } func renamePointer(u *User) { u.Name = "Bob" } func main() { u := User{Name: "Alice"} renameValue(u) fmt.Println(u.Name) renamePointer(&u) fmt.Println(u.Name) }
Go’s Declaration Syntax
考えたこともなかった。確かに自分が読みやすいと感じるKotlinもTypeScriptも右に型を書いてるなとなった。
Pointers to structs
package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} p := &v fmt.Println(v) p.X = 1e9 fmt.Println(v) }
&を付けることでpはvの参照を得ることができる- そうしたときに
(*p).Xとか書くのはアホらしいのでGoでは普通にp.Xとかけるよ
ということが確認できる。以下出力。
{1 2}
{1000000000 2}
package main import "fmt" func main() { var primes []int = [6]int{2, 3, 5, 7, 11, 13} var s []int = primes[1:4] fmt.Println(s) }
スライスは配列への参照なので、スライスの要素を書き換えると元の配列の要素も書き変わる
make, append – Sliceのcapを超えた時
makeは無からSliceを作る。
package main import "fmt" func main() { a := make([]int, 5) printSlice("a", a) b := make([]int, 0, 5) printSlice("b", b) c := b[:2] printSlice("c", c) d := c[2:5] printSlice("d", d) d = append(d, 1) printSlice("d", d) } func printSlice(s string, x []int) { fmt.Printf("%s len=%d cap=%d %v\n", s, len(x), cap(x), x) }
cap の場合 → 倍にするcap >= 1024の場合 → 約 1.25倍ずつ増やす- つまり
appendは「新しい配列にコピーしてcapを増やす」という動作もしている
a len=5 cap=5 [0 0 0 0 0] b len=0 cap=5 [] c len=2 cap=5 [0 0] d len=3 cap=3 [0 0 0] d len=4 cap=6 [0 0 0 1]
range – データ構造をiterateする
package main import "fmt" var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} func main() { for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } }
mapの操作
package main import "fmt" func main() { m := make(map[string]int) m["Answer"] = 42 fmt.Println("The value:", m["Answer"]) m["Answer"] = 48 fmt.Println("The value:", m["Answer"]) delete(m, "Answer") fmt.Println("The value:", m["Answer"]) v, ok := m["Answer"] fmt.Println("The value:", v, "Present?", ok) }
goにクラスはない/methodを作るにはstruct+レシーバー引数を作る
package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) }
GoのMethods continued vs Kotlinの拡張関数
Goのメソッドは、structで定義した型以外の型に対しても定義できる。*2
package main import ( "fmt" "math" ) type MyFloat float64 func (f MyFloat) Abs() float64 { if f 0 { return float64(-f) } return float64(f) } func main() { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs()) }
GoのMethods continuedはメソッドは元の型に「属する」形で定義されるが、Kotlinの拡張関数は元の型に属さず定義されるあくまで拡張関数。なので、Kotlinの拡張関数はオーバーライドされない。
変数レシーバーとポインタレシーバー
package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f } func main() { v := Vertex{3, 4} v.Scale(10) fmt.Println(v.Abs()) }
- ポインタレシーバーであれば、レシーバ自体が持つ値を変更できる
- 変数レシーバーはできない
つまりポインタレシーバーで定義されたメソッドはレシーバーに対して副作用があるかもしれないが、変数レシーバで定義されたメソッドはレシーバーに対して副作用がないということになる。
またポインタレシーバーが巨大な場合に変数レシーバーを多用すると毎度変数コピーのオーバーヘッドが大きくなる。
一般的には変数レシーバ または ポインタレシーバのどちらかですべてのメソッドを与え、混在させるべきではない
インターフェースの実装時に破綻するから。
type Printer interface { Print() } type Incrementer interface { Increment() } type Counter struct { n int } func (c Counter) Print() { fmt.Println(c.n) } func (c *Counter) Increment() { c.n++ } func main() { var p Printer = Counter{} var i Incrementer = Counter{} }
iに代入しようとしても、Counterは値のためfunc (c *Counter) Increment() { c.n++ }を満たさず、代入できない。
package main import "fmt" func main() { var i interface{} = "hello" s := i.(string) fmt.Println(s) s, ok := i.(string) fmt.Println(s, ok) f, ok := i.(float64) fmt.Println(f, ok) f = i.(float64) fmt.Println(f) }
switchによる分岐もできる。
package main import "fmt" func do(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Twice %v is %v\n", v, v*2) case string: fmt.Printf("%q is %v bytes long\n", v, len(v)) default: fmt.Printf("I don't know about type %T!\n", v) } } func main() { do(21) do("hello") do(true) }
goで代数的データ型は表現できる?
直和型(enum)がないから無理。よってdefaultは実質すべてpanic扱い。
package main import "fmt" type Expr interface{} type Add struct { Left, Right Expr } type Number struct { Value int } func Eval(e Expr) int { switch v := e.(type) { case Number: return v.Value case Add: return Eval(v.Left) + Eval(v.Right) default: panic(fmt.Sprintf("unknown expression: %T", v)) } } func main() { expr := Add{ Left: Number{1}, Right: Add{ Left: Number{2}, Right: Number{3}, }, } result := Eval(expr) fmt.Println(result) }
なんでGoにはenumがないんだろう?
このissueがそう。
- Goのデフォルト値の問題
- 外部入力時の処理
ここがハードルになっているっぽい。たしかに未定義のenumを何として扱うか?は悩ましいかも。けどnilでいいような気もするが…🤔
type error interface { Error() string }
事前定義されたこいつを使って独自エラーを定義する。
package main import ( "fmt" "time" ) type MyError struct { When time.Time What string } func (e *MyError) Error() string { return fmt.Sprintf("at %v, %s", e.When, e.What) } func run() error { return &MyError{ time.Now(), "it didn't work", } } func main() { if err := run(); err != nil { fmt.Println(err) } }
Goroutine
最初の一歩
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") }
Channels
- チャネルへ値を送る
- チャネルから受信した変数を代入
- チャネルを作る
チャネルは
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } fmt.Println("this is sum = ", sum) c // send sum to c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) fmt.Println("A") go sum(s[len(s)/2:], c) fmt.Println("B") x, y := // receive from c fmt.Println("C") fmt.Println(x, y, x+y) }
何回か実行した出力。非同期に処理されたのがわかる。
# 1 A B this is sum = -5 this is sum = 17 C -5 17 12 #2 A B this is sum = 17 this is sum = -5 C 17 -5 12 #3 A this is sum = 17 B this is sum = -5 C 17 -5 12
Buffered Channels
バッファ付きのチャネル。
package main import "fmt" func main() { ch := make(chan int, 2) ch 1 ch 2 ch 3 fmt.Println(結果
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /tmp/sandbox2970589188/prog.go:9 +0x58Range and Close
close(ch)でチャネルを閉じるv, ok := でokがfalseならチャネルは閉じている
package main import ( "fmt" ) func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) v, ok := c fmt.Println(v, ok) for i := range c { v2, ok2 := c fmt.Println(v2, ok2) fmt.Println(i) } v3, ok3 := c fmt.Println(v3, ok3) }
出力。おそらくcloseが非同期で走ったタイミングからfalseに変わっている。
0 true 1 true 1 3 true 2 8 true 5 21 true 13 0 false 34 0 false
Select
準備ができたcaseのうち実行できるものを実行する。caseが複数あればランダムに決まる。掲載されているサンプルコードで動きはわかったけど、これだと絶対最後にcase でreturnとしゴルーチンが終わるので、あんまりいいサンプルじゃない。
package main import "fmt" func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c case "quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i 10; i++ { fmt.Println(0 }() fibonacci(c, quit) }
- 文法や考え方がめっちゃシンプルで覚えやすい
- 基本文法は1日あればひとさらいできた(気がする)
- “A Tour of Go”が教材として優秀説はある
- あとよくわからんところとか疑問点は都度ChatGPTに教えてもらってた
interfaceが他言語と考え方が違って少し戸惑った。特にポインタレシーバーあたり- 文法や考え方がめっちゃシンプルなのがわかったので、今の状態で『初めてのGo言語』や三井さん(
id:todays_mitsui)にもらった『効率的なGo』を読むとめっちゃ面白く読めそう - いろんな言語の勉強を継続的にしてるとプログラミングに必要な要素が大体わかるのでキャッチアップにかかる時間が年々短くなっていってる気がする*3
たぶん普通にソースコードは読めるようになったと思うので、強化学習とお作法の習得のためにechoを解析してみる。
ついでにIDE(GoLand)にも慣れていく。
元の記事を確認する
