バックエンドでGo Langを使う案件や会社のお付き合いが最近ぐっと増えたため、Go Langの基礎文法を学びます。A Tour of Goをざっと回したので、メモ。
変数の型推論と初期化
:=を使うことで、変数の型推論と代入が可能。
「短変数宣言(short variable declaration)」と言うらしい。また、この宣言はグローバルにはできず、関数内部が最大のスコープとなる。
a,b,c := 10, "10", false;
簡単にdeferが使える
TypeScriptでは、async/awaitが必須のdeferがこれだけで実現できるのは驚き。意味合い的には、try-catchのfinallyに近いなぁ。
deferは後入れ先出しで、最後にdeferしたものから順番に実行される。
package main import "fmt" func main() { fmt.Println("start") defer fmt.Println("end") // ← 最後に実行される fmt.Println("middle") }
固定長の配列は、array。可変長はslice
a := [3]int{1, 2, 3} s := make([]int, 3)
for文は、rangeを使う
// for..in的なループ // 自動的に第1引数にindexが入る for index, value := range items { fmt.Println(index, value) } // index使わない場合はこ for _ , v := range nums { fmt.Println(i, v) } m = make(map[string]int) // mapはこうなる for key, value := range m { fmt.Println(key, value) } // Pythonと同じでStringも回せる for i, r := range "GoLang" { fmt.Println(i, string(r)) }
Goの関数はクロージャ
func counter() func() int { x := 0 return func() int { x++ return x } } func main() { c := counter() fmt.Println(c()) // 1 fmt.Println(c()) // 2 fmt.Println(c()) // 3 }
cはxの内容を常に記憶してるわけですな。
Struct
- Goにはクラスがない。それは全然OK。クラスに紐づけた関数を作る場合は、レシーバーつき関数を作ることで代替するらしい。謎い。
- 参照渡しをしないと、Structで定義した変数の中身が変わらない。値渡しをするケースがあまり想像できないな。
type User struct { Name string } func (u User) ChangeByValue() { u.Name = "Value" } func (u *User) ChangeByPointer() { u.Name = "Pointer" } func main() { user := User{Name: "Init"} user.ChangeByValue() fmt.Println(user.Name) // Init (変更されない) user.ChangeByPointer() fmt.Println(user.Name) // Pointer(変更される) }
interface
implementsがない。シグネチャが同じなら同じTypeだとみなされるようだ。へー。
type Greeter interface { Greet() string } type User struct { Name string } // Structに紐づいた関数Greetが、interfaceのGreetと同じシグネチャ // UseはGreeter interfaceを実装したものとみなされる func (u User) Greet() string { return "Hello, " + u.Name } func PrintGreeting(g Greeter) { fmt.Println(g.Greet()) } func main() { u := User{Name: "Taro"} PrintGreeting(u) // User は勝手に Greeter とみなされる }
アサーション
- 変数がどの型かを示す構文がちゃんとある。
- Switch文でパターンマッチ的なこともやれるが、TSほど厳密じゃない。
var x any = "hello" // okで型がマッチしたかどうかが代入される // 例外が存在しないGoらしい考え方だ。 s, ok := x.(string) fmt.Println(s, ok) // "hello" true
// x が取りうる型の「Union」を型制約として定義 type Printable interface { ~int | ~string | ~[]int } // x は Printable だけに限定 func printType[T Printable](x T) { switch v := any(x).(type) { case int: fmt.Println("int:", v) case string: fmt.Println("string:", v) case []int: fmt.Println("slice:", v) default: fmt.Println("impossible (for now)") } }
GoRoutine
- これが一番Go言語の強みに見える。
- チャネルという機構を使って、
Pub/Subする。 - チャネルはスレッドセーフで、同期される。受信しないとルーチンの実行で止まる。
- selectを使うことで、チャネルのパターンマッチができる。
ch := make(chan int) // {}()で無名関数を作成し、funcに渡している // goroutine を作るには「関数を渡す必要」がある go func() { ch <- 10 // 10 を送信 }() v := <-ch // 10 を受信 fmt.Println(v)
func fetch(ctx context.Context, ch chan string) { select { case ch <- "done": case <-ctx.Done(): fmt.Println("canceled:", ctx.Err()) } }
Error
- 例外機構がないから、throwもない。
- 例外を投げることを禁じることで、何かしらエラーを表現する型を返す文化。ええやん。
- interfaceだから
Error()をメソッドで実装して任意の型を表現するのが良さそう。
type error interface { Error() string } type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message) }
まとめ
- Goの思想、すこ。例外なんていらん。
- 関数型にかぶれている僕は全部immutableにしたいときがありますが、Goは並行処理も強いし、レシーバーの値渡しがそれに近いので、基本、ミュータブルでよさげ。
- Goroutineが最も特徴的な言語仕様だ。なるほどなぁ。並行処理がマストなバックエンドでよー使われるのがわかる。
- あとは実際にWebアプリケーション作ってみて、API/DBなどの非同期I/Oをどうさばいていくのか、それぐらいかな。
素晴らしいまとめ
このエントリのお陰で更に解像度あがった。