【Go入門】初めてのGo言語
はじめに
オライリー社の初めてのGo言語でGoに入門してみました。
2章 基本型
- 変数
- := で宣言した変数は代入可能
- 未使用変数があるとコンパイルエラーが発生する
- 未使用定数が関数外で定義されている場合はコンパイルエラーにならない
- コンパイル時にバイナリに含まれないようになる
3章 合成型
-
配列
- 固定長、
append
で拡張不可、変数代入でコピー作成可能
array0 := [3]int {1, 2, 3} array1 := [...]int {1, 2, 3, 4}
-
[3]int
の配列と[4]int
の配列は異なる型として扱われる - サイズが異なる配列への変換もできない
- 事前にサイズが分かる場合のみ配列が使える
- そういった制限があるため配列はあまり使われない
- 配列は「スライス」の後方支援のために使われる
- 固定長、
-
スライス
- 可変長、
append
で拡張可能、コピーする場合はcopy()
を使用する
slice0 := []int {1, 2, 3} slice1 := make([]int, 3, 5)
-
append
は、x = append(x, 2)
といった形で戻り値を変数代入しないとエラーになる- Goでは関数の引数に値のコピーを渡すため、
append
に渡しているx
はx
のコピーであるため、代入する必要がある
- Goでは関数の引数に値のコピーを渡すため、
- 各スライスはキャパシティを持っている
-
append
していくとlen
とcap
が返す値が変わったりする - あらかじめ最大サイズがわかっているなら
make
を使った方が効率がよい
-
- 余談:Goのランタイムは、コンパイル時にバイナリファイルに組み込まれる
- コンパイラ:高水準で書かれたJavaやGoのプログラムをバイナリファイルに変換する
- ランタイム:バイナリファイル実行時にサポートしてくれるやつ。メモリ管理とか並列処理をしてくれる
- 可変長、
-
make
- 長さとキャパシティをあらかじめ定義できる
make(len, cap?)
-
len
を指定したスライスに対してappend
を実行すると、len
で指定した要素分は0が並んでしまうので注意が必要 - キャパシティの必要性
- パフォーマンス向上
- キャパシティがあることでスライスに要素を追加をする際にメモリ再割り当ての頻度が低下する。メモリ再割り当ては比較的高コストである。
- メモリ効率の向上
- 余分なメモリ割り当てずに済む
- パフォーマンス向上
- 長さとキャパシティをあらかじめ定義できる
-
スライス生成方法の選び方
- 全く大きくならない可能性がある場合
- nilスライス
var data []int
- nilスライス
- 初期値もしくは固定値が入る場合
- スライスリテラル
data := []int{2,4,6,8}
- スライスリテラル
- 実行時にはスライスサイズが予想できるが、開発中には予測できない場合
- make
- 筆者としては、スライス長さ0で初期化してappend使うのが好みらしい。パフォーマンスは若干落ちる可能性があるかもしれないが、余分な0が並んだり、長さを超えたインデックスアクセス時のpanicなどのバグ可能性が減るから。
- 全く大きくならない可能性がある場合
-
map
- key-valueの形で定義する
m := map[int]bool{}
- 存在しないkeyにアクセスするとゼロ値が返される
- カンマokイディオムを使用することで、keyが存在しているかどうかを確認できる
v, ok = m["notExistKey"] fmt.Println(v, ok) // 0 false
- key-valueの形で定義する
-
構造体
- mapは、キーの値が全て同じ型である必要があるので限界がある
- 構造体の宣言方法
type person struct { name string age int }
- 無名構造体
- 外部データを構造体に変換するアンマーシャリング、逆に構造体をJSONなどの外部データに変換するマーシャリングでよく使用する
// 変数personに対する無名の構造体を定義 var person struct { name string age int } person.name = "bob" person.age = 20 fmt.Println(person) // 変数初期化と無名構造体定義を同時に行う pet := struct { name string kind string }{ name: "pochi", kind: "dog", } fmt.Println(pet)
4章 ブロック、シャドーイング、制御構造
-
ブロック
-
変数のシャドーイング(隠蔽)に注意する必要がある
- シャドーイングとは、外側のブロックで宣言された変数を内側で再代入した場合、以降内側のブロックでは、外側で宣言された変数にアクセスできなくなる
- 対策としては、shadowというリンターを入れる
- ちなみに、Goは25個のキーワードしか持たない小さな言語で、このキーワードにstringやint、true, false, makeなどは含まれていない。ユニバースブロックで、あらかじめ宣言・定義されている識別子として扱っている
- つまり、stringなどもシャドーイングされてしまうリスクがあるということなので思いがけないバグを仕込まないように注意が必要
-
if
- if-elseどちらでも有効なローカルな変数を定義できる
- 下記の
n
がそれ
- 下記の
rand.Seed(time.Now().Unix()) if n := rand.Intn(10); n == 0 { fmt.Println("小さすぎる", n) } else if n > 5 { fmt.Println("大きすぎる", n) } else { fmt.Println("良い感じ", n) } // compile error fmt.Println(n)
- if-elseどちらでも有効なローカルな変数を定義できる
-
for
- 他の言語と異なり、Goではリストに対するループ処理はforのみ
- for-rangeはよく使いそう
evenVals := []int{2,4,6,8,10,12} for i, v := range evenVals { fmt.Println(i, v) }
- 返り値を複数定義できる
- 複数の場合はカンマ区切りでreturn句を書く
func multiplication(num1, num2 int) (int, error) { if num1 == 0 || num2 == 0 { return 0, errors.New("0は渡さないで") } return num1 * num2, nil } func main() { fmt.Println(multiplication(2, 10)) fmt.Println(multiplication(2, 0)) }
-
返り値に名前をつけることもできる
- 下記のような理由から、基本的には使用を避けた方がよい
- シャドーイングが起きる可能性が上がる&無視ができてしまうので可読性が落ちる
- ブランクreturnした時に、名前付き返り値に最後に代入された値が返る
- 名前付きで値を返す関数ならば、絶対に!ブランクreturnを使ってはいけない!
- 実際に何が返されるかを、コードを遡って、戻り値に最後何が代入されたのかを確認しなければいけなくなるから
- 下記のような理由から、基本的には使用を避けた方がよい
-
クロージャ
- 関数の引数に関数を渡したり、関数の返り値を関数にしたりできる
-
defer
- 関数の終了時まで実行が延期される
- 後入れ先出しなので、最後にdeferしたものが先に実行される
- return文の後に実行されるので、defer文が返す値を知る方法はない
- deferの中で、外側の関数の戻り値を検証したりするのは「名前付き戻り値」を使用する
- deferに無名の即時実行関数を定義し、名前付き返り値である
err
を見ている例↓
- deferに無名の即時実行関数を定義し、名前付き返り値である
func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string) (err error) { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer func() { // 返り値であるerrをdefer関数の中で参照/代入できている if err == nil { err = tx.Commit() } // 略... }() return nil }
-
Goは値渡し
- 関数に引数を渡す際は、引数の値をコピーしている
- なので、関数の中で引数を上書きする処理をして、その関数を実行した後でも、上書きしようとした値は変更されない(構造体や基本型の場合)
- ただし、スライスとマップの場合は変更される
- スライスとマップは、内部的にはポインタを渡しているため
- ポインタを渡したら、なぜ上書きされるのかは6章で説明する
-
6章 ポインタ
-
ポインタ入門
- ポインタとは、ある値が保存されているメモリ内の位置を表す変数
- 位置自体のことをアドレスと呼ぶ
- ポインタは**「別の変数が保存されているアドレス」が入っているただの変数**である
- ポインタはどのような型を参照していてもサイズが同じ
- メモリ内のデータが保存されている位置(アドレス)を示す値であるから
- どんなに実データが小さくても大きくても、アドレスのサイズは変わらない
-
&
はアドレス演算子で「変数の前につけるとその変数のアドレス」を返す -
*
は関節参照の演算子で「ポインタ型の変数の前につけると、そのポインタが参照するアドレスに保存されている実際の値」を返す- nilポインタを
*
で参照しようとするとpanicが発生するので注意 - 関節参照する際は、nilでないことを確認してから参照した方がよい
- nilポインタを
- 基本型のリテラルや定数は、コンパイル時にのみに存在して、メモリ内にアドレスを持たないため、それらの前に
&
をつけることはできない- これを回避するには、定数を保持する変数を作ることorポインタを返すヘルパー関数を作成すること
-
ポインタはミュータブル(変更可能)の印
- Goは値渡しなので、関数に渡されるのはコピー=基本型や構造体、配列などの非ポインタ型では、関数は元のデータを変更できない
- 一方、関数にポインタを渡すと、関数が受け取るのはそのポインタのコピー=コピーも同じデータを参照するため、呼び出された側の関数で元データの変更可能になる
- つまり、ポインタを使って引数がミュータブルであることを示すことができる
- 関数の中で、渡された引数のアドレスにある値を変更したい場合は、渡されたポインタの間接参照(*でアクセス)して、値を代入する
func mod(addr *string) { // 渡されたアドレスを間接参照して値を代入する *addr = "forced_update" } func main() { str := "Hello,World" mod(&str) fmt.Println(str) // forced_update }
-
ポインタは最終手段
- ポインタはデータの流れがわかりにくくなるので使う際は慎重になるべき
- 基本は、関数にポインタを渡して構造体の中身を埋めるのではなく、関数が構造体のインスタンスを返すように実装すべきである
- ポインタ引数を使わなければいけないのは、関数がインターフェイスを受け取る時だけで、よくあるのはJSONを扱うパターン
-
ポインタ渡しのパフォーマンス
- 構造体のサイズが大きくなると、引数として渡すのも戻り値とし戻すのもポインタの方がパフォーマンスが良くなる
- ポインタを関数に渡す場合、データサイズによらず一定で1ナノ秒
- とは言っても、ポインタを使うか値を使うかの違いは、パフォーマンスにはそれほど影響しない(影響はあるが、それほど気にしなくて良い影響という意)
- メガバイトものデータをやり取りする場合は、データがイミュータブルであっても、ポインタの使用は検討しても良いでしょう
- 構造体のサイズが大きくなると、引数として渡すのも戻り値とし戻すのもポインタの方がパフォーマンスが良くなる
-
マップとスライスの違い
- マップ
- (前の章で見たように)関数に渡されたマップに対する変更は元のマップに反映される
- 理由:マップは構造体へのポインタとして実装されているため
- すなわち、関数にマップを渡す=ポインタをコピーする
- 特に外部に公開するAPIでは、マップを入力用パラメータに使用したり、値を返すのに使ったりするのは避けた方がよい
- 理由:マップにどのようなキーが定義されているかを明示的に定義するものがないので、コードを読んでもらう必要があるため
- (感想)関数のインプットとアウトプットには、構造体を使いましょう的な感じ?
- (前の章で見たように)関数に渡されたマップに対する変更は元のマップに反映される
- スライス
- マップより複雑
- スライスの内容の変更は、元のスライスに反映されるが、appendを使ってサイズを変更しようとしても、元のスライスは変更されない
- スライスが3つのフィールド(サイズ/キャパシティ/メモリブロックを参照するポインタ)を持つ構造体として実装されているため
- スライスの内容を変更した場合は、中身を示すポインタが参照しているメモリの値が書き換えられるため、元のスライスからも見ることができる
- 一方、サイズやキャパシティは、コピーに対して行われるだけで、元のスライスには反映されない
- スライスが3つのフィールド(サイズ/キャパシティ/メモリブロックを参照するポインタ)を持つ構造体として実装されているため
- マップ
7章 型、メソッド、インターフェース
- Goは静的に型付けされる言語である
- Goは継承ではなく、合成を推奨している
- Goの型
-
抽象型と具象型
- 抽象型:型が何をするものかを定義するが、どのようにするかは規定しない
- 具象型:何をどのようにするかまでを規定する
- Goで使われる全ての型は、抽象型もしくは具象型に当てはまる
-
メソッド
- 構造体に付随する関数を定義できる
type Person struct { LastName string FirstName string Age int } func (p Person) String() string { return fmt.Sprintf("%s %s : 年齢%d歳", p.LastName, p.FirstName, p.Age) } func main() { person := Person{ LastName: "Yamada", FirstName: "Taro", Age: 30, } fmt.Println(person.String()) }
-
キーワードfuncとメソッド名の間にレシーバを追加する
-
レシーバに型名(
Person
)を書くことで、このメソッドが型Person
に結びつけられる- レシーバの命名は、pなどの先頭1文字がよく使われる
-
レシーバの種類
- レシーバにはポインタ型と値型がある
- 以下のような制約がある
- メソッドがレシーバを変更する場合、ポインタレシーバを使用しなければならない
- メソッドがnilを扱う必要がある場合、ポインタレシーバを使用しなければならない
- メソッドがレシーバを変更しないなら、値レシーバを使うことができる
- 型にポインタレシーバが1つでもあるなら、レシーバを変更しないものも含め、ポインタレシーバに揃えるのが一般的
type Counter struct { total int lastUpdated time.Time } func (c *Counter) Increment() { c.total++ c.lastUpdated = time.Now() } func (c Counter) String() string { return fmt.Sprintf("合計: %d, 更新: %v", c.total, c.lastUpdated) } func main() { var c Counter fmt.Println(c.String()) // &cと書かなくて良い c.Increment() fmt.Println(c.String()) }
-
c.Increment()
のようにポインタレシーバに対して値型を渡した場合、Goが自動的にポインタ型に変換してくれるため、&c.Increment()
と書かなくて良い - Goでは、ゲッターやセッターは書かずにフィールドに直接アクセスすることを推奨している
-
nilへの対応
- 値レシーバのメソッドにnilを渡すと、メソッドを起動しようとしてパニックになる
- ポインタレシーバのメソッドは、メソッドがnilハンドラを書いていれば、有効な呼び出しになる
-
関数とメソッドの使い分け
- ここでのメソッドは構造体に定義している型メソッドのことを指している
- ロジックが、起動時に設定された値や実行中に変更された値に依存する場合、そのような値は構造体に保存されるべきであり、メソッドとして定義すべき
- 一方、ロジックが入力引数にのみ依存するなら、関数とすべき
-
iotaと列挙型
- iotaはenumみたいなもの
const ( Uncategorized MailCategory = iota // 未分類 // 0が割り当てられる Personal // 個人的 // 1 Spam // 迷惑 Social // ソーシャル Advertisement // 広告 ) func main(){ fmt.Println(Uncategorized) // 0 fmt.Println(Personal) // 1 }
- 定数の値が他のシステムやデータベースの値として使われている(仕様的に数値が意味を持っている)場合は、iotaを使ってはいけない
- 列挙型に新たに値を追加することを禁止する仕組みはない+リストの途中に追加した場合は以降の値は再割り当てされるので、思わぬバグの可能性がある
- あくまで外に出ない、内部的な目的に限る
-
埋め込みによる合成
- Goには継承はないが、合成や昇格があり、これを使ったコードの再利用を推奨している
- 下記の
Manager
構造体にEmployee
と書くことで、埋め込むことができる- 埋め込まれた
Employee
のフィールドやメソッドが昇格する
- 埋め込まれた
// 従業員 type Employee struct { Name string ID string } func (e Employee) Description() string { return fmt.Sprintf("%s (%s)", e.Name, e.ID) } // マネージャー type Manager struct { Employee // 型のみを書くと埋め込みフィールドになる(NameとIDが追加される) Reports []Employee // 報告対象者(部下) } func (m Manager) FindNewEmployee() []Employee { newEmployees := []Employee{ { "石田三成", "1", }, { "徳川家康", "2", }, } return newEmployees } func main() { m := Manager{ Employee: Employee{ Name: "豊臣秀吉", ID: "3", }, Reports: []Employee{}, } fmt.Println(m.ID) // 3 fmt.Println(m.Description()) // 豊臣秀吉 (3) m.Reports = m.FindNewEmployee() fmt.Println(m.Employee) // {豊臣秀吉 3} fmt.Println(m.Reports) // [{石田三成 1} {徳川家康 2}] }
-
埋め込みと継承の違い
- 継承と同じものだと理解しようとすると間違いの元になってしまう
- 上記のManager型の変数をEmployee型の変数に割り当てることは不可
var eOK Employee = m.Employee // OK fmt.Println(eOK) var eNG Employee = m // Error
-
インタフェース
-
インターフェースとは
- Go言語で最も注目されている機能は並行実行モデルだが、筆者的最大の注目点は「暗黙のインターフェース」
- インターフェースは、Goで実装を提供しない唯一の抽象型である
- 宣言方法
// インターフェース宣言 インターフェースもtypeの一種 type Stringer interface { String() string }
-
型の集合を定義する
-
型の一種である
-
メソッドリストを書く
-
インターフェースの名前は、通常「〜するもの・人」を示す、「er」で終わる(
io.Reader
,io.Closer
など) -
これらの特徴は、他の言語のインターフェースとあまり変わりはない
-
が、特別なのは「暗黙的に」実装されることである
- 「暗黙的に」というのは、具象型(実装)の方は、あるインターフェースを実装するかどうかを「明示的に」宣言しない
-
interface
で宣言したメソッドと同じシグネチャ(名前、引数、戻り値)を実装すると、暗黙的にそのinterface
を実装するということ
-
そもそも多くの言語になぜインターフェースがあるのか?
- インターフェースに従って実装することで、必要に応じて実装を交換することができるから
- (感想)つまり、保守性と安全性(インターフェースに従っていればバグは発生しづらい)が向上するって感じ?
-
Goのインターフェースは型安全なダックタイピングである
- ダックタイピングとは、型や名前ではなく、振る舞いに焦点を当てて、その振る舞い(メソッド)を呼び出せるなら可能という、動的型付け原gのでの概念
- 一方、Javaなどの静的型付け言語では、インターフェースを定義し、そのインターフェースを実装する。つまり使う側としては、インターフェースだけを参照している
- Goは、これら2つのスタイルを混ぜ合わせたものである
-
暗黙的にインターフェースを実装する例
-
Person
がSpeak
メソッドを実装することで、暗黙的にSpeaker
インターフェースを実装している
// Speaker インターフェース type Speaker interface { Speak() string } // Person 型が Speaker インターフェースを暗黙的に実装 type Person struct { Name string } // Person 型に Speak メソッドを定義 func (p Person) Speak() string { return fmt.Sprintf("Hello, my name is %s", p.Name) } func main() { // Person 型が Speaker インターフェースを実装 var speaker Speaker = Person{Name: "Alice"} // Speak メソッドを呼び出す fmt.Println(speaker.Speak()) }
- (感想)明示的なインターフェースより柔軟で、ダックタイピングより安全であるという感じなのだろうけど、現時点ではあまりメリットが分からない..
-
-
空インターフェース
- 前までは
interface{}
だったが、any
でも表現可能になった - よく使われるケース1:JSONファイルのような外部ソースから読み込まれた、形式が不明なデータの記憶場所
data := map[string]any{} // string->anyのマップで要素なし contents, err := os.ReadFile("sample.json") if err != nil { fmt.Println(err) } json.Unmarshal(contents, &data) fmt.Println(data)
- よく使われるケース2:ユーザーが生成したデータ構造の中に値を保存すること
- でもなるべくanyを使うのは避けましょう
- 前までは
-
インターフェース型の変数が特定の具象型を持っているか、あるいはその具象型が別のインターフェースを実装しているかを調べるには2つの方法がある
-
- 型アサーション
-
i
とMyInt
は同じ型でないとパニックになる- たとえ基底型が同じでもNG
- パニックを回避するには、カンマokイディオムを使用する
- 型アサーションを使用する際は、パニックが発生していないと確信があっても、カンマokイディオムを使うようにしよう
type MyInt int func main() { var i any var mine MyInt = 20 i = mine i2 := i.(MyInt) // 型アサーション i2はMyInt型になる // i2 := i.(int) // panic! fmt.Println(i2) // カンマokイディオムを使用してパニックを回避する i3, ok := i.(int) if !ok { err := fmt.Errorf("iの型(値:%v)が想定外です", i) fmt.Println(err.Error()) os.Exit(1) } fmt.Println(i3) }
-
2.型switch
func doTypeSwitch(i any) { // 引数iの型を取得する switch j := i.(type) { case nil: fmt.Println("iはnilです") case int: fmt.Printf("%dはintです", j) } }
-
-
暗黙のインタフェースによる依存性注入
- Goでは、依存性注入の実装が容易で、追加のライブラリも必要ない
- 暗黙のインタフェースを使って依存性注入を実現可能
-
-
8章 エラー処理
-
エラー処理の基本
-
関数が期待通りに実行された場合、errorには
nil
が返される -
呼び出し側で、errorの戻り値のnilチェックを行い、そのエラーを処理したり、独自のエラーを返したりする
-
エラーを返す場合は、他の戻り値はゼロ値を返すべきである
-
呼び出し側でエラーを検知する仕組みはないので、関数の戻り値を見てnilかどうかを見る必要がある
-
エラー生成は、errorsパッケージの関数Newを使用する
errors.New(”error!”)
-
errorは組み込みのインタフェースでメソッドを1つだけ定義している
- このインタフェースを実装するものはerrorと見なされる
- エラーがないことにnilを返すのは、nilがインタフェースのゼロ値であるため
type error interface { Error() string }
-
Goで、例外スローではなく、エラーを返す理由
- 例外があると、コードのパスが1本新たに加わるため
- 大抵このパスは分かりづらく、適切な例外処理がされずにクラッシュを引き起こす原因になりうる
- 例外があると、コードのパスが1本新たに加わるため
-
-
単純なエラーの際の文字列の利用
- 1.
error.New
で文字列を受け取り、errorを返す- 呼び出し側で、errorを
fmt.Println
に渡すと自動でErrorメソッドが呼ばれ、文字列(エラーメッセージ)が出力される
- 呼び出し側で、errorを
- 2.
fmt.Errorf
を使用する
func doubleEven(i int) (int, error) { if i % 2 != 0 { // 1 return 0, errors.New("処理対象は偶数のみです") // 2 return 0, fmt.Errorf("%dは偶数ではありません", i) } return i * 2, 0 }
- 1.
-
センチネルエラー
- sentinel自体は「(見張りをして)守る、保護する」みたいな意味
- センチネルエラーの説明の前に背景として..
- Goではerrorがinterfaceとして宣言されていて、
Error() string
メソッドを実装すれば、errorとして扱える - 一方、errorの中身によってハンドリングしたい時、上記のstringをparseしてやらんといけないというのはプログラム的には扱いづらいよね
- Goではerrorがinterfaceとして宣言されていて、
- センチネルエラーとは
- パッケージレベルで宣言されるエラー値が格納された変数群
- 特定のエラーの際に固定された値を返すことでハンドリング容易にする
- 慣習的に
Err
というprefixがつくErrFormat
など
- 参考:Goのカスタムエラーとその自動生成について
-
エラーと値
- errorはインタフェースなので、独自のエラーを定義できる
- 例えば、ステータスコードを含めることができる
type Status int const ( InvalidLogin Status = iota + 1 // 1 NotFound // 2 ) type StatusErr struct { Status Status Message string } // Errorメソッドを定義することでerrorインタフェースを暗黙的に実装 func (se StatusErr) Error() string { return se.Message } // カスタムエラーを返す場合でも返り値としてはerror型を定義する func login(uid, pwd string) (error) { // 適当なログイン処理 err := loginPwd(uid, pwd) if err != nil { return StatusErr{ Status: InvalidLogin, Message: fmt.Sprintf("invalid for user %s", uid), } } return nil }
-
エラーのラップ
- エラーに付加的な情報を追加することをラップと言う
-
fmt.Errorf
の%w
を使用することで、オリジナルエラーも含むエラーを生成することができる - ラップされたエラーをアンラップする
unWrap
関数もあるようだけど、あまり使わなそうなので割愛
-
IsとAs
- エラーのラップは、付加的な情報を得るための方法としては有用だが、センチネルエラーがラップされると
==
でチェックすることができなくなる、カスタムエラーの型アサーションやswitchでマッチできなくなるという問題点もある- この問題を解決するのがパッケージ
errors
のIs
とAs
である
- この問題を解決するのがパッケージ
- Is
- 戻されたエラーが特定のセンチネルエラーのインスタンスとマッチするかをチェックする
- As
- 戻されたエラーが特定の型にマッチするかをチェックする
- エラーのラップは、付加的な情報を得るための方法としては有用だが、センチネルエラーがラップされると
-
defer
を使ったエラーのラップ- 戻り値に名前errをつけて、
defer
で参照できるようにする - 同じメッセージで複数のエラーをラップしたい場合に使える
- 戻り値に名前errをつけて、
-
パニックとリカバー
-
Goのランタイムが次に何をすれば良いのか判断できない時にパニックが生成される
- スライスの要素数を超えて読み込もうとした時
- メモリ不足が発生した時
-
パニックが発生すると
- 実行中の関数は即座に終了する
-
defer
されていたものが実行される - mainに到達する
- メッセージとスタックトレースを表示してプログラムが終了する
-
独自のパニックも生成できる
func doPanic(msg string) { panic(msg) } func main() { doPanic(os.Args[0]) } // log % go run -trimpath hello.go main.doPanic(...) ./hello.go:8 main.main() ./hello.go:12 +0x4c exit status 2
-
パニックを補足する方法として、リカバーが用意されている
- より静かに終了したり、そもそも終了しないようにできる
- つまり、パニック時の回復処理をやってくれるやつ
-
組み込み関数
recover
は、defer
内でパニックが起こったかをチェックする目的で使える-
recover
はdefer
内で呼び出さなければいけない - パニックが起こっていればそのパニックに際して代入された値が返される
- パニックになると通常実行される部分は実行されr図、deferされた部分だけが実行される
- 下記を例にすると、60 / 0という計算は実行されずに、エラーメッセージを出力する処理のみを実行している
func div60(i int) { defer func() { if v := recover(); v != nil { fmt.Println(v) } }() fmt.Println(60 / i) } func main() { for _, val := range []int{1,2,0,6} { div60(val) } } // log % go run -trimpath hello.go 60 30 runtime error: integer divide by zero 1
-
-
パニックが起こってしまってから、実行を継続することは滅多にないはず
-
メモリやディスク不足でパニックが発生した場合、
recover
を使って、現状をログに書き出し、os.Exlt(1)
でプログラムを終了するのが最も安全である -
panic
とrecover
には依存しない方が良い- 理由:メッセージを出力して継続するだけなので、なぜ失敗したかが分からないため
-
recover
が推奨されるのは、サードパーティ用のライブラリを開発している時で、公開APIの境界を超えて、パニックを伝播させてはいけない- パニックの可能性があるなら、
recover
を使ってエラーに変換すべき - そのエラーを受けて、呼び出し側でどうするのかを決めてもらうのが良い
- パニックの可能性があるなら、
-
-
エラー時のスタックトレース
- Go初心者は
panic
とrecover
を使いたがる- 理由:スタックトレースを取得したいため(Goはデフォルトでスタックトレースを表示しない)
- サードパーティライブラリ
errors
はコールスタックを自動生成してくれるのでそれを使おう
- Go初心者は
9章 モジュールとパッケージ
-
リポジトリ、モジュール、パッケージ
- Goのライブラリは、大きい方から「リポジトリ」「モジュール」「パッケージ」という3つの概念を使って管理される
- リポジトリ:プロジェクトのソースコードが保存される場所
- モジュール:Goのライブラリもしくはアプリケーションのrootになり、リポジトリに保存される
- 1リポジトリには1モジュールが推奨されている
- パッケージ:モジュールを構成する(モジュールに1つもしくは複数含まれる)
- 標準ライブラリ以外のパッケージのコードを利用するには、自分のプロジェクトをモジュールとして宣言する必要がある
- モジュールはグローバルでユニークな識別子を持つ
- Goでは、通常リポジトリへのパスを使用する(ex:
github.com/jonbodner/proteus
)
- Goのライブラリは、大きい方から「リポジトリ」「モジュール」「パッケージ」という3つの概念を使って管理される
-
モジュールとgo.modファイル
- Goをモジュールとして扱うにはルートディレクトリにgo.modファイルが必要になる
-
go mod init MODULE_PATH
コマンドで生成可能
-
- Goをモジュールとして扱うにはルートディレクトリにgo.modファイルが必要になる
-
パッケージの構築
-
インポートとエクスポート
- 識別子の先頭を大文字にすることで、宣言されたパッケージの外側からアクセス可能になる
-
パッケージの作成と構築
- ファイルの最初の行がパッケージ節になる
package math func Double(a int) int { return a * 2 }
- 標準ライラブリ以外からインポートする場合はインポートパスが必要
- 可読性やリファクタ観点で絶対パス記述するのが推奨
- go mod tidy コマンドで外部モジュールをダウンロードしてくれる
- パッケージ名は、具体的に内容を表現するように命名する
- utilなどは避ける
-
モジュールの構成方法
- cmd
- アプリケーションコマンド群
- エントリーポイントが配置される
- アプリケーションごとにサブディレクトリを切る
- サブディレクトリの中にはmain.goを置く
- pkg
- パッケージ群
- 機能のまとまりごとにサブディレクトリを切る
- ECを例にすると、顧客管理と在庫管理..のような形
- 参考になりそう
- cmd
-
init関数
- パッケージ内の初期化タスクを実行する(自動実行)
- 引数や戻り値は無し
- 内部処理であり、外から実行されることを通常避ける
- 用途としては、DB接続や設定ファイルの読み込み、HTTPハンドラの登録など
-
サードパーティーのコードのインポート
- Goは、他の多くのコンパイラ言語とは異なり、アプリケーションを1つのバイナリファイルにまとめる
- サードパーティーのパッケージをインポートする際は、パッケージが置かれているリポジトリの場所を指定する
-
go mod init {module}
で新しいモジュールの追加および依存関係の更新が可能-
module
には、Githubリポジトリのパスやドメイン名、パス名が入る - その後、
go mod tidy
を実行することで、依存関係の整理(不要な依存関係を削除する等)を行なってくれる
-
- 上記を実行すると、依存関係のセキュリティと整合性を確保する
go.sum
が作成される- モジュールのパッケージとチェックサムが記載されている
- モジュールプロキシサーバ
- Goでは中央集権的なリポジトリ(ex:
npm
)には依存せず、すべてのモジュールはGithub等のリポジトリに保存されている - デフォルトでは、go getはリポジトリからコードを直接フェッチせず、Googleが運営しているプロキシサーバとモジュールのやりとりをする
- 加えて、Googleはチェックサムデータベースを保守しており、すべてのモジュールのすべてのバージョンの改ざんを防止している
- Goでは中央集権的なリポジトリ(ex:
-
10章 並行処理
- はじめに
- そもそも並行処理や並行性とは「ひとつの処理を独立した複数のコンポーネントに分割し、コンポーネント間で安全にデータを共有しながら計算すること」である
- 他の多くの言語では、並行性をライブラリを介して提供していて、ライブラリでは多くの場合、OSレベルのスレッドによって、ロックを使ってデータを共有している
- 並行性をいつ利用すべきか?
- 並行性が高まれば比例して速くなるわけではないし、何より理解しづらいプログラムが出来上がる可能性が上がる
- 並行性を利用すべきなのは「独立に処理できる複数の操作から生成されるデータ」を利用する場合である
- ゴルーチン
- 用語の整理
- プロセス:プログラムがOSによって実行されているもの(OSは何らかのメモリなどのリソースとプロセスを関連付け、他のプロセスがそのリソースにアクセスできないようにする)
- スレッド:1プロセスを構成するタスクのこと。実行の単位であり、OSから決められた時間が与えられて実行される。
- ゴルーチン:Goのランタイムで管理される軽いスレッドのこと。
- Goのプログラムが開始される時、ランタイムがプログラムが実行するためのいくつかのスレッドを生成し、一つのゴルーチンを起動する
- 起動されたゴルーチンは、Goのランタイムスケジューラによってスレッドに割り当てられる
- 何千、何万ものゴルーチンを同時に動かすことができる
- Goのプログラムが開始される時、ランタイムがプログラムが実行するためのいくつかのスレッドを生成し、一つのゴルーチンを起動する
- ゴルーチンは、ビジネスロジックをラップするクロージャとともに起動するのが一般的である
- つまり、ビジネスロジック自体をゴルーチンで実装するのではなく、それをラップして呼び出す関数でゴルーチンを起動するようにするということっぽい
- そうすることで、責務が分担され、テストが容易になる
- 用語の整理
- チャネル
-
基本
- ゴルーチンでは、情報のやり取りにチャネルを使用する
- ゴルーチンはチャネルにデータを送信し、別のゴルーチンはそのデータを受信するイメージ
- チャネルは組み込みの型である
- マップ同様にチャネルは参照型である
- 関数にチャネルを渡すと、実際にはポインタを渡すことになる
- ゴルーチンでは、情報のやり取りにチャネルを使用する
-
読み込み、書き込み、バッファリング
- 読み込みには、チャネル変数の左側に
<-
を置く(a := <-ch
) - 書き込みには、チャネル変数の右側に
<-
を置く(ch <- b
) - チェネルに書き込まれた値は一度だけ読み込むことができるので、同じチャネルから複数のゴルーチンが読み込みを行っている場合、チャネルに送信された値は、そのうちのひとつのゴルーチンからのみ読み込まれる
- ひとつのゴルーチン関数が、同じチャネルに対して読み書き両方行うのは一般的ではない
- 受信専用と送信専用のチャネルを用いるのが一般的
- バッファリングとは一時的にチャネルでデータを保持すること
- デフォルトでバッファリングはしない
- バッファリングされるチャネルを作成するにはmake関数の第2引数にキャパシティを渡す:
ch := make(chan int, 10)
- ほとんどの場合はバッファリングされないチャネルを使用すべき
- 読み込みには、チャネル変数の左側に
-
チャネルのクローズ
- チャネルへの書き込みが終わったら、組み込み関数
close(ch)
でクローズする - クローズされたチャネルに書き込もうとしたり、同じチャネルを二度クローズするとパニックが発生する
- 上記は
sync.WaitGroup
を使って対処可能
- 上記は
- 一方クローズされたチャネルからの読み込みは常に成功する
- カンマokイディオムで、チャネルがクローズされたかどうか検知できる
- チャネルをクローズする責任を持つのは「書き込み側」である
- チャネルがクローズするのを待っている時のみ、クローズ処理は必ず書くようにする(for-rangeループを使ってチャネルから読み込みをしている場合等)
- 独立したゴルーチンが逐次的に処理を行い、チャネルを介してのみやり取りするので、並行性に関する煩雑さを軽減してくれる
- 他の言語では、グローバルに共有された状態に依存してスレッド間でやり取りするので、データフローの理解がややこしくなる
- チャネルへの書き込みが終わったら、組み込み関数
-
select
-
select
を使用することで、複数のチャネルに対する読み込みや書き込み操作が可能になる- ゴルーチン間でデータの同期や複数のイベントを監視できる
- 例:複数のチャネルからデータを受信し、どれか1つかが利用になった時点で処理を実行する、みたいなことができる
- 書き方はswitch文に似てる
- ただswitchとは異なり、case句が上から評価されるわけではない
- 複数のcaseが実行可能だった場合、どれか1つのcaseがランダムで実行される
// デッドロックが発生する例(実行結果: fatal error: all goroutines are asleep - deadlock!) func main(){ ch1 := make(chan int) ch2 := make(chan int) go func(){ v := 1 ch1 <- v // ch1から読み込まれないと次に進めない v2 := <-ch2 // ch2から読み込む fmt.Println(v, v2) }() v := 2 ch2 <- v // ch2から読み込まれないと次に進めない v2 := <- ch1 // ch1から読み込む fmt.Println(v, v2) } // デッドロックが発生しない例(実行結果: mainの最後 2 1) func main(){ ch1 := make(chan int) ch2 := make(chan int) go func(){ v := 1 ch1 <- v // 1がch1に書かれる v2 := <-ch2 fmt.Println("無名関数", v, v2) }() v := 2 var v2 int select { case ch2 <- v: // これはまだ書き込まれない case v2 = <- ch1: // 1がch1に入れがこれは実行される(v2は1になる) } fmt.Println("mainの最後", v, v2) }
- 上記のようにデッドロックを防止してくれる
- for-selectループ
- よく使われるパターン
- 複数のチャネルの中で前に進めるものを選択してくれる
-
-
- 並行処理のベストプラクティスとパターン
- (外に公開する)APIに並行性は含めない
- 並行性は実装に関する詳細なので外には出さないようにすべき
- 利用者にチャネルのバッファリングやクローズの管理をさせるべきではないため
- (外に公開する)APIに並行性は含めない
- ゴルーチンの終了チェック
- ゴルーチンとして実行される関数を起動する際には、確実に終了しなければいけない
- Goのランタイムは全く使われないゴルーチンの検知はできない
- ゴルーチンが終了しない場合、スケジューラは定期的にゴルーチンに何もしないための時間を割り振り、全体の動作が遅くなる(ゴルーチンリーク)
- ゴルーチンとして実行される関数を起動する際には、確実に終了しなければいけない
- doneチャネルパターン
- ゴルーチンが完了したことを通知し、メインゴルーチンがそれを待つためのパターン
- いつバッファ付きのチャネルを使うべきか
- バッファ付きのチャネルは動作も複雑になるし、扱いも難しい
- WaitGroupの利用
- 複数のゴルーチンの処理の終了を待ちたい時は
sync
パッケージのWaitGroup
を使用する- 参考:ゴールーチンの待ち合わせ
- WaitGroupは便利だが、ゴルーチン協調のための第1の選択肢にすべきではない
- 複数のゴルーチンの処理の終了を待ちたい時は
- 参考サイト
-
Goでの並行処理を徹底解剖!
- これめっちゃわかりやすい..
-
Goでの並行処理を徹底解剖!
12章 コンテキスト
- コンテキストとは
-
APIの境界を超えたり、プロセス間でリクエストにまつわるメタデータを扱う機構がコンテキストである
- 主に一つの処理が複数のゴールーチンをまたいで行われる場合に使用される
-
コンテキストは、関数の最初の引数に
ctx
という変数名で渡す慣習がある -
まずは空のコンテキストを生成し、そこにメタデータを追加していく形が一般的
// Backgroundメソッドで空の初期コンテキストを作成する ctx := context.Backgroud() logic(ctx, data)
-
HTTPサーバーを記述する場合は、ミドルウェアを使用する少し異なるパターンで実装する
-
- キャンセレーション
- 複数のゴルーチンを起動し、それぞれが異なるHTTPサービスを呼び出すリクエストがある処理で、あるサービスが有効な結果を返せないようなエラーを返した場合、他のゴルーチンの処理を停止することをキャンセレーションという
- キャンセル可能なコンテキストは
context.WithCancel(ctx)
で生成する- 引数には親となるコンテキストを渡す
- 生成されたコンテキストのCancel関数を実行することで、他のコンテキスト配下に処理の停止を伝播させる
- キャンセル関数は生成したら、処理の成功失敗に限らず、必ず実行しないといけない
- 理由:実行しないとプログラムがリソースをリークし、最終的には動作が遅くなったりクラッシュしたりするため
- コンテキストによる値の伝搬
-
context.WithValue
でコンテキストに値を関連付けられる - 関連付けを行う関数は
ContextWith
で始まる名前をつけるのが一般的 - ユースケースとしてはユーザー情報やトラッキングID
- 例:cookieからユーザーIDを抽出して、ミドルウェアでユーザーを取得する
-
- 参考サイト
13章 テスト
- テストの基礎
-
Goのテストは製品版のコードと同じディレクトリ・パッケージに置かれる
-
テストファイルは
_test.go
-
テスト関数の名前は
Test
で始め、*testing.T
型の引数を一つだけ取る(引数名は慣習的にt
という命名が一般的) -
go test
コマンドでカレントディレクトリにあるテストを実行する -
結果が正しくない時は
t.Error
メソッドでエラーをレポートする-
Error
はそのテスト関数内の後続の処理を継続するので、テストが落ちた時点で処理を停止したい場合はFatal
メソッドを使用する
-
-
各テスト関数同士で設定情報などの変数を共有したい場合は、
TestMain
関数を使用する- データベースの接続情報やテスト対象のコードが初期化を必要とするパッケージレベルの変数に依存する場合など
-
テスト関数内で生成したリソースを解放した場合は
t.Cleanup
メソッドを使用する- 引数には、引数および戻り値がない関数を渡す
-
defer
のようなもので最後に実行される - 例えば、ファイル作成を行う関数をテストする際のファイル削除など
-
テスト用サンプルデータの保存
-
testdata
というサブディレクトリを作成する - 相対パスでアクセスする
-
-
テスト結果のキャッシング
- 成功しているテストは複数パッケージにまたがっていてもコード変更がなければ結果はキャッシュされる
-
testdata
のファイルが変更された場合はテストは再実行される -
go test
コマンドに-count=1
オプションを渡すと、テストを強制実行できる
-
go-cmp
によるテスト結果の比較- 複合データ型を比較する際はサードパーティモジュールである
go-cmp
が便利 - 比較してマッチしない部分の詳細な説明を返してくれる
- 構造体のプロパティが欠損している実装例↓
// cmp.go package cmp import ( "time" ) type Person struct { Name string Age int DateAdded time.Time } func CreatePerson(name string, age int) Person { return Person{ Name: name, Age: age, DateAdded: time.Now(), } }
// cmp_test.go package cmp import ( "testing" "github.com/google/go-cmp/cmp" ) func TestCreatePerson(t *testing.T) { expected := Person{ Name: "Dennis", Age: 37, } result := CreatePerson("Dennis", 37) if diff := cmp.Diff(expected, result); diff != "" { t.Error(diff) } }
% go test --- FAIL: TestCreatePerson (0.00s) cmp_test.go:17: cmp.Person{ Name: "Dennis", Age: 37, - DateAdded: s"0001-01-01 00:00:00 +0000 UTC", + DateAdded: s"2023-11-20 17:47:41.01171 +0900 JST m=+0.002319418", } FAIL exit status 1
- 複合データ型を比較する際はサードパーティモジュールである
-
テーブルテスト
- テーブルテストを用いることで、1つの関数に対して複数のテストケースを書く場合、同じ処理を繰り返し書くことになることを防ぐ方法
- 実装方法は、テストケースの名前や引数、期待値などを格納したスライスをループし、ループ内で
testing.T
のRun
メソッドを実行する - go test -vとすることで、各サブテストの名前も表示される
-
コードカバレッジ
-
go test -cover
で、カバレッジ情報が出力される -
go tool cover -html=c.out
で、テストが実行されていない箇所を強調したHTMLファイルが出力される
-
-
ベンチマーク
- ベンチマークを調べるテスト関数は、名前に
Benchmark
というプレフィックスをつけ、*testiing.B
という引数を受け取る関数にする -
go test -bench=.
で全てのベンチマークを実行する- 更に
-benchmem
フラグを渡すことで、メモリ割り当て情報を含めることができる
- 更に
- ベンチマークを調べるテスト関数は、名前に
-
Discussion