💡

【Go入門】初めてのGo言語

2024/01/31に公開

はじめに

オライリー社の初めての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に渡しているxxのコピーであるため、代入する必要がある
    • 各スライスはキャパシティを持っている
      • appendしていくとlencapが返す値が変わったりする
      • あらかじめ最大サイズがわかっているならmakeを使った方が効率がよい
    • 余談:Goのランタイムは、コンパイル時にバイナリファイルに組み込まれる
      • コンパイラ:高水準で書かれたJavaやGoのプログラムをバイナリファイルに変換する
      • ランタイム:バイナリファイル実行時にサポートしてくれるやつ。メモリ管理とか並列処理をしてくれる
  • make

    • 長さとキャパシティをあらかじめ定義できる make(len, cap?)
    • lenを指定したスライスに対してappendを実行すると、lenで指定した要素分は0が並んでしまうので注意が必要
    • キャパシティの必要性
      • パフォーマンス向上
        • キャパシティがあることでスライスに要素を追加をする際にメモリ再割り当ての頻度が低下する。メモリ再割り当ては比較的高コストである。
      • メモリ効率の向上
        • 余分なメモリ割り当てずに済む
  • スライス生成方法の選び方

    • 全く大きくならない可能性がある場合
      • nilスライス var data []int
    • 初期値もしくは固定値が入る場合
      • スライスリテラル 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
    
  • 構造体

    • 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)
      
    • for

      • 他の言語と異なり、Goではリストに対するループ処理はforのみ
      • for-rangeはよく使いそう
      evenVals := []int{2,4,6,8,10,12}
      for i, v := range evenVals {
      	fmt.Println(i, v)
      }
      

    5章 関数

    • 返り値を複数定義できる
      • 複数の場合はカンマ区切りで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を見ている例↓
      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でないことを確認してから参照した方がよい
    • 基本型のリテラルや定数は、コンパイル時にのみに存在して、メモリ内にアドレスを持たないため、それらの前に&をつけることはできない
      • これを回避するには、定数を保持する変数を作ること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つのフィールド(サイズ/キャパシティ/メモリブロックを参照するポインタ)を持つ構造体として実装されているため
          • スライスの内容を変更した場合は、中身を示すポインタが参照しているメモリの値が書き換えられるため、元のスライスからも見ることができる
          • 一方、サイズやキャパシティは、コピーに対して行われるだけで、元のスライスには反映されない

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つのスタイルを混ぜ合わせたものである
          • 暗黙的にインターフェースを実装する例

            • PersonSpeakメソッドを実装することで、暗黙的に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つの方法がある

            1. 型アサーション
            • iMyIntは同じ型でないとパニックになる
              • たとえ基底型が同じでも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.error.Newで文字列を受け取り、errorを返す
      • 呼び出し側で、errorをfmt.Printlnに渡すと自動で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
    }
    
  • センチネルエラー

    • sentinel自体は「(見張りをして)守る、保護する」みたいな意味
    • センチネルエラーの説明の前に背景として..
      • Goではerrorがinterfaceとして宣言されていて、Error() stringメソッドを実装すれば、errorとして扱える
      • 一方、errorの中身によってハンドリングしたい時、上記のstringをparseしてやらんといけないというのはプログラム的には扱いづらいよね
    • センチネルエラーとは
      • パッケージレベルで宣言されるエラー値が格納された変数群
      • 特定のエラーの際に固定された値を返すことでハンドリング容易にする
      • 慣習的に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でマッチできなくなるという問題点もある
      • この問題を解決するのがパッケージerrorsIsAsである
    • Is
      • 戻されたエラーが特定のセンチネルエラーのインスタンスとマッチするかをチェックする
    • As
      • 戻されたエラーが特定の型にマッチするかをチェックする
  • deferを使ったエラーのラップ

    • 戻り値に名前errをつけて、deferで参照できるようにする
    • 同じメッセージで複数のエラーをラップしたい場合に使える
  • パニックとリカバー

    • 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内でパニックが起こったかをチェックする目的で使える

      • recoverdefer内で呼び出さなければいけない
      • パニックが起こっていればそのパニックに際して代入された値が返される
      • パニックになると通常実行される部分は実行され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)でプログラムを終了するのが最も安全である

    • panicrecoverには依存しない方が良い

      • 理由:メッセージを出力して継続するだけなので、なぜ失敗したかが分からないため
    • recoverが推奨されるのは、サードパーティ用のライブラリを開発している時で、公開APIの境界を超えて、パニックを伝播させてはいけない

      • パニックの可能性があるなら、recoverを使ってエラーに変換すべき
      • そのエラーを受けて、呼び出し側でどうするのかを決めてもらうのが良い
  • エラー時のスタックトレース

    • Go初心者はpanicrecoverを使いたがる
      • 理由:スタックトレースを取得したいため(Goはデフォルトでスタックトレースを表示しない)
    • サードパーティライブラリerrorsはコールスタックを自動生成してくれるのでそれを使おう

9章 モジュールとパッケージ

  • リポジトリ、モジュール、パッケージ
    • Goのライブラリは、大きい方から「リポジトリ」「モジュール」「パッケージ」という3つの概念を使って管理される
      • リポジトリ:プロジェクトのソースコードが保存される場所
      • モジュール:Goのライブラリもしくはアプリケーションのrootになり、リポジトリに保存される
        • 1リポジトリには1モジュールが推奨されている
      • パッケージ:モジュールを構成する(モジュールに1つもしくは複数含まれる)
    • 標準ライブラリ以外のパッケージのコードを利用するには、自分のプロジェクトをモジュールとして宣言する必要がある
      • モジュールはグローバルでユニークな識別子を持つ
      • Goでは、通常リポジトリへのパスを使用する(ex: github.com/jonbodner/proteus
  • モジュールとgo.modファイル
    • Goをモジュールとして扱うにはルートディレクトリにgo.modファイルが必要になる
      • go mod init MODULE_PATHコマンドで生成可能
  • パッケージの構築
    • インポートとエクスポート

      • 識別子の先頭を大文字にすることで、宣言されたパッケージの外側からアクセス可能になる
    • パッケージの作成と構築

      • ファイルの最初の行がパッケージ節になる
      package math 
      
      func Double(a int) int {
        return a * 2
      }
      
      • 標準ライラブリ以外からインポートする場合はインポートパスが必要
        • 可読性やリファクタ観点で絶対パス記述するのが推奨
      • go mod tidy コマンドで外部モジュールをダウンロードしてくれる
      • パッケージ名は、具体的に内容を表現するように命名する
        • utilなどは避ける
    • モジュールの構成方法

    • init関数

      • パッケージ内の初期化タスクを実行する(自動実行)
      • 引数や戻り値は無し
      • 内部処理であり、外から実行されることを通常避ける
      • 用途としては、DB接続や設定ファイルの読み込み、HTTPハンドラの登録など
    • サードパーティーのコードのインポート

      • Goは、他の多くのコンパイラ言語とは異なり、アプリケーションを1つのバイナリファイルにまとめる
      • サードパーティーのパッケージをインポートする際は、パッケージが置かれているリポジトリの場所を指定する
      • go mod init {module} で新しいモジュールの追加および依存関係の更新が可能
        • moduleには、Githubリポジトリのパスやドメイン名、パス名が入る
        • その後、go mod tidy を実行することで、依存関係の整理(不要な依存関係を削除する等)を行なってくれる
      • 上記を実行すると、依存関係のセキュリティと整合性を確保するgo.sumが作成される
        • モジュールのパッケージとチェックサムが記載されている
      • モジュールプロキシサーバ
        • Goでは中央集権的なリポジトリ(ex: npm)には依存せず、すべてのモジュールはGithub等のリポジトリに保存されている
        • デフォルトでは、go getはリポジトリからコードを直接フェッチせず、Googleが運営しているプロキシサーバとモジュールのやりとりをする
        • 加えて、Googleはチェックサムデータベースを保守しており、すべてのモジュールのすべてのバージョンの改ざんを防止している

10章 並行処理

  • はじめに
    • そもそも並行処理や並行性とは「ひとつの処理を独立した複数のコンポーネントに分割し、コンポーネント間で安全にデータを共有しながら計算すること」である
    • 他の多くの言語では、並行性をライブラリを介して提供していて、ライブラリでは多くの場合、OSレベルのスレッドによって、ロックを使ってデータを共有している
  • 並行性をいつ利用すべきか?
    • 並行性が高まれば比例して速くなるわけではないし、何より理解しづらいプログラムが出来上がる可能性が上がる
    • 並行性を利用すべきなのは「独立に処理できる複数の操作から生成されるデータ」を利用する場合である
  • ゴルーチン
    • 用語の整理
      • プロセス:プログラムがOSによって実行されているもの(OSは何らかのメモリなどのリソースとプロセスを関連付け、他のプロセスがそのリソースにアクセスできないようにする)
      • スレッド:1プロセスを構成するタスクのこと。実行の単位であり、OSから決められた時間が与えられて実行される。
      • ゴルーチン: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に並行性は含めない
      • 並行性は実装に関する詳細なので外には出さないようにすべき
      • 利用者にチャネルのバッファリングやクローズの管理をさせるべきではないため
  • ゴルーチンの終了チェック
    • ゴルーチンとして実行される関数を起動する際には、確実に終了しなければいけない
      • Goのランタイムは全く使われないゴルーチンの検知はできない
      • ゴルーチンが終了しない場合、スケジューラは定期的にゴルーチンに何もしないための時間を割り振り、全体の動作が遅くなる(ゴルーチンリーク)
  • doneチャネルパターン
    • ゴルーチンが完了したことを通知し、メインゴルーチンがそれを待つためのパターン
  • いつバッファ付きのチャネルを使うべきか
    • バッファ付きのチャネルは動作も複雑になるし、扱いも難しい
  • WaitGroupの利用
    • 複数のゴルーチンの処理の終了を待ちたい時はsyncパッケージのWaitGroupを使用する
    • WaitGroupは便利だが、ゴルーチン協調のための第1の選択肢にすべきではない
  • 参考サイト

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.TRunメソッドを実行する
      • go test -vとすることで、各サブテストの名前も表示される
    • コードカバレッジ

      • go test -coverで、カバレッジ情報が出力される
      • go tool cover -html=c.outで、テストが実行されていない箇所を強調したHTMLファイルが出力される
    • ベンチマーク

      • ベンチマークを調べるテスト関数は、名前にBenchmarkというプレフィックスをつけ、*testiing.Bという引数を受け取る関数にする
      • go test -bench=.で全てのベンチマークを実行する
        • 更に-benchmemフラグを渡すことで、メモリ割り当て情報を含めることができる

Discussion