Open12

Goのキャッチアップ

mrmsmrms

はじめに

業務でGo言語を使用するため、初めてのGo言語を用いてキャッチアップを行う。
このスクラップでは、学習しながらポイントをメモしていく。

mrmsmrms

1章

ビルド

Goの場合、ビルドを行うと実行形式のバイナリファイルが作成されるため、配布して共有しやすい。
実行形式のバイナリファイルのため古いバージョンのGoでできたものでも問題なく動作する。

fmt

Goでは標準の形式にフォーマットしてくれるコマンド(fmt)が用意されている。
goimportsという強化版(フォーマット以外に不要なインポートの削除なども行う)もある。

lint, vet

goのリンターにstaticcheckがあり、インストールして使用可能。
メソッドに渡す引数の数が間違っていたり、使われない変数に値を代入しているなどを検出するのにgo vetコマンドが使用できる。
golangci-lintには上記のようなツール群が用意されており、同時に適用できる。ツールが増えると指摘も増えるためどれを適用するかは適宜調整は必要。
使うとしたらgo vet, staticcheck, golangci-lint`の順で適用する。

Makefile

makeコマンドによってビルドを自動化する際に使用されるファイル。
コマンド実行によって記載されている通りにチェックやビルドが行われる。
fmtlintなどを記載しておくこともできる。

mrmsmrms

2章

ゼロ値

宣言のみで値が割り当てられていない変数には、ゼロ値が代入される。型によって決まっている。

リテラル

  • 整数リテラル

  • 浮動小数点数リテラル

  • runeリテラル
    文字を表す。'で囲む。

  • 文字列リテラル

    • 解釈済み文字リテラル
      "で囲む。エスケープしていない\、改行、"は使用できない。
      "解釈済み文字リテラル"
      
    • 生の文字リテラル
      `で囲む。改行、"が使用できる。
      `生の文字
      "リテラル"`
      

整数型

int8 ~ int64, uint8 ~ uint64(uintは符号なしのint。)がある。

特別な整数型

  • byte
    uint8の別名。
  • int
    CPUによって64ビット、32ビットになる。
  • uint
    intの符号なし(0以上)
  • rune
    コードポイントを表現する。
    コードポイント

    文字集合(あ、い・・・などの文字の集合体、Unicodeなど)に対して順番に割り振った数値。
    コードポイントは文字集合によっては異なるが、符号化方式(UTF-8, UTF-16)による違いはない。
    文字集合と符号化方式の違いはこちらを参照。

  • uintptr
    メモリを操作してポインタ操作を可能にするもためのもの。

整数値の選択

以下のルールで選択すると良い。

  • 要件で数値の大きさや符号が決まっているならそれに合わせる。
  • 全ての整数型に機能するライブラリは、int64, uint64の2種類を作成する。
  • その他の場合はintを使用する。

浮動小数点数型

float32とfloat64がある。
浮動小数点数型は誤差が発生しやすいため、金額計算などには不向き。

変数の宣言

変数宣言の使い分け

  • ゼロ値の初期化は「var x int」の形式を使い、意図を明確にする
  • 型指定されていないリテラルを変数に代入するとき、デフォルトの型が希望する変数の型と異なる時は「var x int」の形式を使う
  • 新しく宣言した変数であることを明確にする場合は、varを使った宣言の後に「=」で値を代入する
  • 複数の変数を一行で宣言するのは、複数の値を返却する関数の使用時と「カンマok イディオム」の場合にする

変数の宣言は関数の外では行わない。

定数

constで宣言する。
Goのconstは、リテラルに名前をつけるためのもの。
そのため、代入できるのは以下のもののみ。

  • 数値リテラル
  • true, false
  • 文字列
  • rune
  • 組み込み関数(complex, real, imag, len, cap)の結果
  • 上記に挙げた値と演算子で構成される式

Goには実行時に計算された値がイミュータブルであることを指定する方法はない。

mrmsmrms

6章

ポインタ

ポインタは、値が保存されているメモリ内の位置(アドレス)を表す変数。

var x int32 = 10
var y bool = true

と宣言した時、変数は1バイト(最小単位)から何バイトかの連続したメモリに保存されている。
その位置をアドレスという。
xはint32なので4バイトを使って記憶され、yはブール値のため1バイトを使用して記憶される。
アドレス番号を1から始まるとすると、xは1から4までのアドレス、yは5のアドレスに記憶される。
ポインタ自体も変数のため、中身は「別の変数が保存されているアドレス」である。

ポインタのゼロ値は、nil
スライス、マップ、関数のゼロ値がnilなのは、ポインタを使って実装されているため。
ポインタを数値に変換したり、数値をポインタに変換することはできない。
GoではC,C++とは異なり、ポインタに関する演算などのポインタ操作は許可されていない。

「&」は、アドレス演算子で変数の前につけるとその変数のアドレスを返す。
返却された値は、ポインタ型の値になる。

「*」は、間接参照のための演算子。
ポインタ型の変数の前につけると、ポインタが参照しているアドレスに保存されている値を返す(デリファレンス)。

x := 10
pointerToX := &x
z := 5 + *pointerToX // z = 15

ポインタ型の変数をvarを使って宣言するには、「そのポインタが示す先に保存される値の型」の前に「*」をつけて表す。
上記を図にすると以下のイメージ。

組み込み関数のnewでポインタ型の変数を生成できる。指定された型のゼロ値を値とするインスタンスへのポインタを返す。

var a = new(int) 

このnewはほとんど使用されない。

構造体は構造体リテラルの前に「&」をつけてポインタのインスタンスを作る。
基本型のリテラル(1, true, "aaa"など)や定数の前に「&」をつけることはできない。なぜならコンパイル時にのみ存在し、メモリ内のアドレスを持たないため。

// 以下はできない
&10
&true
&"aaa"

基本型へのポインタが必要な場合は、基本型の変数を宣言し、それを参照するポインタ変数を宣言する必要がある。変数であればメモリ上にアドレスがあるため。

var y string
z := &y

構造体のフィールドに基本型へのポインタがあると上記の仕様のせいでリテラルを直接代入できない。
こういう場合はヘルパー関数を使用するのが良い。

type person struct {
    FirstName string
    MiddleName *string
    LastName string
}

p := person{
    FirstName: "A",
    MiddleName: "B",  // コンパイルエラー。リテラルはメモリ内のアドレスがない。
    LastName: "C", 
}

// ヘルパー関数
// 関数の引数、つまり変数に代入することでアドレスが存在する
func stringp(s string) *string {
    return &s
}
// ヘルパー関数を使用
p := person{
    FirstName: "A",
    MiddleName: stringp("B"),  // コンパイルエラーにならない
    LastName: "C" ,
}

Goは、値渡しの言語のため、関数に渡される値はコピーである。そのため、ポインタを使用することでミュータブル(変更可能)であることを示す。
ポインタが引数になっていれば、その関数内で渡された値と同じ値を参照するため関数内で呼び出し元の値が変更可能になる。
関数内でポインタ型の引数をデリファレンスしたものに値を代入することで、呼び出し元の値を変更できる。

ポインタの利用シーン

引数を変更するためにポインタを使用する関数がインターフェースを受け取る時と並行実行に使用されるデータ型の時。
返却値としてポインタを返すのは、データ型の中に変更するべき状態があるとき。

ポインタ渡しのパフォーマンス

ポインタ渡しの場合、かかる時間は一定(アドレスの値のため)だが、データ量が小さい場合は値渡しの方がパフォーマンスが良い。
ただ、よほどデータ量が多い場合を除き、基本的に値渡しで良い。

ゼロ値と値なし

Goでのポインタの使用方法に、ゼロ値が設定されているフィールドと値が全く代入されていない「値なし」のフィールドの区別に使用するものがある。プログラム内でこれを区別するために、nilポインタを使用する。
このパターンを使用するときは、関数からnilに設定したポインタを返すのではなく、「カンマokイディオム」のように値とブール値を返すようにする。
引数としてnilを渡したり、引数の構造体の中にnilのフィールドがある場合、それに値を設定できないことに注意する。
JSONの変換の場合は例外で、「値なし」に設定できる構造体のフィールドにポインタを使う。
JSONを使っていないのであれば(値を変更しないのであれば)、「値なし」を示すのに値とブール値を返すようにする。

マップとスライス

マップは構造体へのポインタとして実装されている。
関数にマップが渡されることはマップのポインタが渡されることになるため、関数内でマップの内容を変更すると呼び出し元のマップも変更される。
そのため、入出力のパラメータに使用しないこと。

スライスは、サイズ、キャパシティ、ポインタのフィールドを持つ構造体のため、コピーを作成するとポインタの参照するアドレスは同じだが、サイズ、キャパシティが異なるスライスが作成される。
そのため、コピーの中身を変更するとオリジナルの中身も変更される。
コピーのキャパシティの変更を行うと新しいスライスが作成されるため、オリジナルと参照先が変わる。
キャパシティに余裕があり、コピーのスライスにサイズを超える値を追加した時、スライスのサイズが変更されて値が保存されるが、オリジナルのサイズは変わらないため、コピーからしか見れないデータが作成されることになる。
関数内では基本的に引数で渡したスライスは変更されないと考えるべきで、スライスの中身を関数内で変更する場合は、ドキュメントで明示しておかなければならない。

mrmsmrms

7章

Goでは任意のレベルのブロックで型を宣言できるが、その型へのアクセスは定義したスコープ内のみ。
パッケージブロックレベルの型がエクスポートされた場合は例外。

メソッド

ユーザー定義の型に対して関数を定義したもの。

type Person struct {
    LastName string
    FirstName string
    Age int
}

func (p Person) String() string { // (p Person)の部分はレシーバ
    // 実装
}

レシーバがあることで型Personと結び付けられ、他の型の変数がこのメソッドを使用することができない。
レシーバ名にthisselfを使うのはイディオム的ではない。
関数と同じくオーバーロードはできないため、1つの型の中で同じ名前のメソッドは1つのみ。
定義できるのは型と同じパッケージ内。
同じファイル内に定義してあるとわかりやすい。

ポインタ型レシーバと値型レシーバ

レシーバには、ポインタレシーバと値レシーバがあり、以下のルールで使い分ける。

  • メソッドがレシーバを変更するならポインタレシーバ(must)
  • メソッドがnilを扱う必要があれば、ポインタレシーバ(must)
  • メソッドがレシーバを変更しないなら、値レシーバを使うことができる
    その型に関して宣言された他のメソッドにポインタレシーバがあれば、他のメソッドも全てポインタレシーバに揃えるのが一般的。

メソッドの解説で出てきた以下のコードでなぜポインタのフィールドに対して値を加算できるのか疑問だったが、内部で自動参照外し((*c). と変換される)が行われているからだった。

type Counter struct {
	total             int
	lastUpdated time.Time
}

func (c *Counter) Increment() { // cのアドレスが渡される
	c.total++ // ここと
	c.lastUpdated = time.Now() // ここが?となった
}

レシーバがnilの場合、ポインタレシーバは、nilの場合の処理を書くことでパニックにならないが、値レシーバの場合、ポインタが参照している値がないためパニックになる。

メソッド値

メソッドは変数に代入することもでき、関数の引数の型がメソッドの返却値の型と同じであれば引数としてメソッドを渡すこともできる。
このようにメソッドを値として扱えるためメソッド値と呼ばれる。

メソッド式

メソッドから関数を作成でき、これをメソッド式という。
メソッド式は、最初の引数にメソッドが追加されたシグネチャになる。

関数とメソッドの使い分け

ロジックが起動時に設定された値や実行中に変更された値に依存する場合、その値は構造体に保存されるべきなので、メソッドとして実装する。
ロジックが引数の値のみに依存する場合は関数として実装する。

型宣言は継承とは異なる

型定義の際にすでに定義した型も使用できるが、継承のように親子関係があるわけではない。
そのため、以下のようになる。

type Score int
type HighScore Score

var i int = 1
var s Score = 10
var hs HighScore = 20
hs = s // できない
s = i // できない
s = Score(i) // 型変換しているためできる
hhs := hs + 20 // 基底型で使用できる演算子は使用できる

型の存在意義は、基本型に名前をつけることで引数の意味を明確する。また、不適切な値の代入を防ぐことである。

iota

Goには列挙型(enum)の代わりにiotaがある。
ベストプラクティスとしては以下のような使い方。

type MailCategory int // とりうることになる値の型を定義

const (
    Uncategorized MailCategory = iota // 最初だけ値を代入すれば以降の定数にも値が代入される
    Personal
    Spam
    Social
    Advertisements
)

iotaを使用することで0から始まる値がインクリメントされて設定されていく。
定数の値が仕様で明確に指定されている場合はその値を設定すべきでiotaは使用しなくて良い。
定数が名前で参照される場面で使用する。

合成

構造体で定義した型を別の型のフィールドに型のみ記載することで埋め込みフィールドになる。
埋め込みフィールドになると、元の構造体のフィールドやメソッドを埋め込み先の構造体から直接呼び出せる。

type Employee struct {
	Name string
	ID   string
}

func (e Employee) Description() string {
	return fmt.Sprintf("%s (%s)", e.Name, e.ID)
}

type Manager struct { // Descriptionも呼び出せる
	Employee // 型のみ書く(埋め込みフィールド) NameとIDが加わる
	Reports []Employee  // 部下(報告の対象者)  Employeeのスライス
}

同じフィールドが埋め込み先にもある場合は、埋め込まれているフィールドの型を明示しなければならない。

継承との違い

埋め込みフィールドがあるからといって埋め込まれた型として埋め込み先の型を扱うことはできない。
また、埋め込みフィールドと上位の構造体に同名のメソッドがある場合、埋め込みフィールド側のメソッドが呼び出される。

インターフェース

Goで唯一の抽象型。
2つの側面がある。

  1. メソッドの集合(実装すべきメソッドを示す)
  2. 型(任意の型の値を代入できる変数を定義できる)
    よく使われるのはinterface{}(Go 1.18からはanyと書けるようになった)。これは「0個のメソッドが定義された型」となるため、任意の型がこの条件を満たす。
    インターフェースの定義の仕方
type Stringer interface { // インターフェースも型の一種
    String() string // 実装するメソッドのリスト(メソッドセット)
}

インターフェースの名前は通常「er」(〜するもの、〜する人)で終わる。
インターフェースの実装側では、インターフェースを実装していることを明示的に宣言しない。
インターフェース側のメソッドセットを実装側のメソッドセットが含んでいれば、実装側が自動的にインターフェースを実装することになる。
インターフェースも型と同じように埋め込むことができる。

インターフェースは型へのポインタと値へのポインタで構成されているため、値がnilでも型がnilでなければnilにはならない(nilを代入しただけはでは型へのポインタがあれば、nilにはならない)。

var a *int // a == nil (intへのポインタはある)
var i interface {} // i == nil
i == a // i != nil (intへのポインタが代入される 型=*int、値=nil)

型アサーション

インターフェースが特定の具象型を持っているか、具象型が別のインターフェースを実装しているかを調べる方法の1つ。もう一つは型switch。
型アサーションでは変換対象の型と変換後の型が一致しないとパニックになる。「カンマokイディオム」を使用することで回避できる。

type MyInt int

func main() {
	var i any // interface{} と同じ
	var mine MyInt = 20
	i = mine
	i2 := i.(MyInt) // 型アサーション

    i3, ok := i.(int) // カンマokイディオムによるパニック回避
    if !ok {
        // エラー処理
    }
}

型アサーションと型変換の違い

具象型 インターフェース チェックタイミング
型変換 適用可 適用可 コンパイル時
型アサーション 適用不可 適用可 実行時

型アサーション使用時は、アサーションの結果が間違いないとしてもカンマokイディオムを使用するようにする。

型switch

インターフェースが複数の型のいずれかを取る場合、型switchを使う。

switch j := i.(type) {
	case nil:
		
	case int:
		
	case MyInt:
		
	case io.Reader:
		
	case string:
		
	case bool, rune:
		
	default:
	
}

イディオム的な手法として、switch分の対象となる変数を同名の変数に代入するものがある(i := i.(type)

型アサーションや型switchは無闇に使わず、あくまで具象型のインターフェースが別のインターフェースを実装していないか確認したい時に使用する。

mrmsmrms

8章

エラー処理

Goでは関数からerror型を返すことでエラーを処理している。
エラーの場合はそれ以外の返却値はゼロ値を返し、エラー出ない場合はエラーがゼロ値となる。

センチネルエラー

現在の状態に問題があり処理を継続できないことを知らせるのを目的としたエラー。
慣習により、Errで始まる。
宣言はパッケージレベルで行われる。

エラーのカスタマイズ

標準のエラーでは文字列のみのものだが、独自のエラーを定義することも可能。
独自のエラーを返す関数でも型はerrorを使用する。これにより使用側が特定のエラーに依存しない。
独自のエラーを使用する場合、初期化されていない変数を返すような実装にはしない。nilを返すようにする。
独自のエラーの型を変数の型として使用せず、error型として定義する。

エラーのラップ

fmt.Errorfで「%w」を使用することで、フォーマットされた文字列に他のエラーの文字列も含むフォーマットが行われるようになる。
慣習でエラーフォーマットの最後に「: %w」を追加するようになっている。
また標準ライブラリでは、ラップされたエラーを関数Unwrapによってアンラップすることもできる(通常直接呼ぶことはない)。
独自のエラーでUnwrapメソッドを実装することで、エラーをラップできるようになる。

Is と As

エラーがラップされている場合、「==」によるチェックはできない。
パッケージerrorsIsAsを使って解決できる。
エラーが特定のセンチネルエラーのインスタンスにマッチするならerrors.Isを使用する。
デフォルトの比較方法(==)で等しいか判定できない場合は、独自の実装をすることができる。
エラーが特定の型にマッチするかをチェックするならerrors.Asを使用する。
Asもオーバーライドが可能だが、リフレクションが必要なので一般的でない状況でのみ行う(一つの型に一致するときのみマッチさせ、それ以外は戻るような時など)。

deferによるエラーのラップ

同じエラーメッセージなどを複数返しているような場合は、deferをつけた無名関数と名前付きの戻り値によって処理をまとめることができる。

パニック

スライスのサイズ以上を読み込もうとした時などにパニックは生成される。
パニックが発生すると、実行中の関数は終了し、deferのついた関数が実行され、その呼び出し元にdeferのついた関数あれば同じく実行され、という流れでmainに戻るまで続く。
終了する時には、メッセージとスタックトレースを表示する。
組み込み関数を使用することで独自のパニックを生成可能。

リカバー

リカバーをすることでパニックが発生しても通常よりも静かに終了したり、終了自体させないこともできる。
リカバーは、defer内で組み込み関数recoverを使用することでパニックの内容をチェックできる。

パニックとリカバーは、例外処理と似ているが例外と同じ意図があるわけではない。
パニックが起こる時は、処理が継続不可能な状況のため、その後の処理を継続せず、リカバーによって適切に処理を行いプログラムを終了するようにする。

mrmsmrms

9章

モジュールとパッケージ

Goのライブラリは「リポジトリ」「モジュール」「パッケージ」の概念を使って管理される。
リポジトリは、プロジェクトのコードが保存される場所。
モジュールは、ライブラリやアプリケーションのルートとなりリポジトリに保存される。
モジュールは、複数のパッケージから構成される。

標準ライブラリ以外のパッケージのコードを利用するには、モジュールとしてそのプロジェクトを宣言する必要がある。
モジュールは、グローバルにユニークな識別子を持つ。

go.modファイル

go mod initコマンドによりgo.modファイルを生成する。
go.modファイルを生成したディレクトリがそのモジュールのルートとなり、モジュールとなる。

可視性

識別子(定数、変数、型、関数、メソッド、構造体のフィールド)の大文字小文字によってアクセス可能範囲が変わる。
大文字から始まる場合は、宣言外のパッケージからもアクセス可能(パブリック)。
小文字の場合は、宣言したパッケージのみ(プライベート)。

パッケージの構造

ソースファイルの一行目にはパッケージ節が必要。
このパッケージ節は同一ディレクトリ内で同じでなければならない。
パッケージをインポートする際には、モジュールまでのパスとパッケージのディレクトリまでのパスを合わせたパスをしてして行う。
上記の点が紛らわしいので一般的に、パッケージ名とパッケージを含むディレクトリ名は同じにするべき。
以下の場合は異なる名前をつける。

  • mainの場合
  • Goの識別子として有効でない文字がディレクトリ名についているとき(ディレクトリ名を考え直すべき)
  • ディレクトリを使ったバージョニングをサポートするため

パッケージの命名について

パッケージ名は、関数名などと同じく機能に関する情報を提供するような名前をつけるべき。
utilなどのパッケージ名にしてしまうと、それ自体が機能に関する情報を何も提供していないため不適切。
パッケージ名自体をパッケージ内の関数や型などの名前で繰り返すのも不適切(例外は識別子の名前がパッケージ名と同じ場合。sortSortなど。)。

モジュールの構成方法

正式な構成方法はないが、いくつか決まった構成パターンがある。

内部パッケージ

internalという名称のパッケージを作成すると、そのパッケージ内およびサブパッケージ内でエクスポートされた識別子は、直接の親や兄弟の位置にあたるパッケージのみでアクセス可能になる。

init関数

引数なし、返却値なしのinit関数を宣言すると宣言しているパッケージが他のパッケージから参照された一番最初に実行される。
一つのパッケージ内や一つのファイル内で複数のinit関数の定義が可能。
ブランクインポート(インポート時の名前を_にする)を使用すると、インポートしたパッケージのinit関数は起動するが、パッケージ内のエクスポートされた識別子にはアクセスできない。
initの主な用途は、一つの代入文だけでは済まないパッケージレベルの変数の初期化である。
init関数は1パッケージにつき1つにするべき。
init関数内でファイルのロードやネットワークアクセスを行う場合、その旨をドキュメントに記載しておく。

循環参照

Goでは、パッケージ同士の循環参照は許可されない。

エイリアス

既存の型に別名をつけることが可能。
この別名の型は既存の型に型変換なしで代入できる。
エイリアスに対してメソッドやフィールドを変更する場合は、オリジナルの型に変更を加える必要がある。
別パッケージや別モジュールの型のエイリアスも作成できるが、そこからエクスポートされていないメソッドやフィールドの参照はできない。
エイリアスを持てないものとして、パッケージレベルの変数、構造体のフィールドがある。

type T1 struct {
    x int
    S string
}

type T2 = T1 // T2がT1のエイリアス

外部パッケージの利用

コード内でインポート後、go mod init(既存go.modは削除)を行いgo mod tidyを行う。

mrmsmrms

10章

並行処理

並行処理を使用することで必ずしも高速化に繋がるとは限らない。
並行性を利用するべきなのは「独立に処理できる複数の操作から生成されるデータ」を利用する場合。
並行に実行される処理が長い時間を要さない場合、並行性は利用しなくて良い。

ゴルーチン

ゴルーチンを理解するために用語を整理する。

  • プロセス
    プログラムがOSによって実行されているもの。
    リソースとプロセスを関連付けることで他のプロセスがアクセスできないようにする。
  • スレッド
    プロセスの構成要素。プロセスは1つ以上のスレッドから構成される。
    実行の単位。1つのプロセス内のスレッドがリソースへのアクセスを共有する。
    1つのCPUは同時に複数の命令を実行できる。

ゴルーチンはGoのラインタイムによって管理される軽量のスレッド。
OSによるスレッドやプロセスの管理でなくGoのランタイムによって管理されることで以下のメリットがある。

  • ゴルーチンの生成は、OSレベルのリソースを生成していないため、スレッドの生成よりも早い。
  • ゴルーチンの初期のスタックサイズがスレッドのスタックサイズより小さく、必要に応じて大きくなるため、メモリ効率が良い。
  • 全体がプロセス内で行われることで、ゴルーチンのスイッチングの方がスレッドのスイッチングより早い。
  • 同一プロセス内の処理となるため、スケジューリングを最適化できる。

関数呼び出し時にgoキーワードを使用することでゴルーチンとなる。
ビジネスロジックをラップするクロージャとともに起動するのが一般的。

チャネル

ゴルーチンでの情報のやり取りにチャネルを使用する。

ch := make(chan int) // チャネルの生成

a := <- ch // チャネルからの読み込み
ch <- b // チャネルへの書き込み

チャネルに書き込まれた値は一度だけ読み込むことができる。
1つのゴルーチンが同じチャネルに対して読み取りと書き込みの両方を行うのは一般的ではない。
ch <- chan int」と記載すると読み込み専用、「ch chan<- int」と記載すると書き込み専用のチャンネルであることを示す。
上記の記載方法でコンパイラによって、読み込み専用のチャネルに書き込みしていないかなどをチェックできる。

デフォルトでは、チャネルはバッファリングされない。
バッファリングされないチャネルの場合、書き込みを行うゴルーチンは、その値が読み込みを行うゴルーチンによって読み込まれるまで(読み込みの準備ができるまで)ポーズされる。
同様に、読み込みを行うゴルーチンは、別のゴルーチンが読み込み対象の値を書き込むまで(書き込みの準備ができるまで)ポーズする。
バッファリングされるチャネルの場合は上記のようなブロックは発生しない。
以下の生成方法でバッファリングされるチャネルを作成できる。

ch := make(chan int, 10)

組み込み関数のlen,capによってチャネルに関する情報が得られる。
lenはバッファ内に現在何個の値があるか、capは最大のバッファサイズがわかる。
バッファのキャパシティは変更できない。
基本的にはバッファリングされていないチャネルを使うべき。

for - range と チャネル

for - rangeを用いてチャネルからの値の読み込みを行うことができる。

for v := range ch { // チャネルがクローズされるまでループ
    fmt.Println(v)
}

チャネルのクローズ

チャネルへの書き込み後、組み込み関数のcloseによってチャネルを閉じる。

close(ch)

クローズされたチャネルに対して値を書き込もうとしたり、再度クローズしようとするとパニックになる。
クローズされたチャネルから値を読み込もうとする場合は常に成功する。
バッファのあるチャネルの場合は読まれていない値が返却され、バッファの値がない、またはバッファリングされていないチャネルを読み込むとゼロ値が返る。
マップと同様、「書き込まれたゼロ値」と「チャンネルがクローズされていたために返されたゼロ値」をカンマokイディオムによって区別できる。

v, ok := <- ch

okfalseならクローズされているチャネル(値はゼロ値)。

チャネルのクローズは書き込み側のゴルーチンが行う。
チャネルのクローズはクローズを待っているゴルーチンがある時のみ必須。

チャネルの動作

チャネルには状態がある。
それぞれの状態で、読み込み、書き込みクローズを行ったときの振る舞いが異なる。

状態/操作 バッファ無 + 開 バッファ無 + 閉 バッファ有 + 開 バッファ有 + 閉 nil
読み込み(Read) 書き込まれるまでポーズ ゼロ値を返す バッファが空ならポーズ バッファ内に値があればその値を返す。空ならゼロ値 無限にハング
書き込み(Write) 読み込まれるまでポーズ パニック バッファが一杯ならポーズ パニック 無限にハング
クローズ(Close) クローズする パニック クローズする パニック パニック

select

selectを使用することで複数のチャネルに対する読み込みや書き込みが可能になる。

select {
    case v := <-ch1:
        fmt.Println("ch1:", v)
    case v := <-ch2:
        fmt.Println("ch2:", v)
    case ch3 <- x:
        fmt.Println("ch3へ書き込み: ", x)
    case <-ch4:
        fmt.Println("ch4から値をもらったが、値は無視した")
}

caseの記載はデータの準備できている順番に実行されるため、case自体の記載順は実行順とはならない。
selectを使用することで、デッドロックが起こってしまうようなロジックを起きないようにできる。
selectは複数のチャネルの中で前に進めるものを選択する特性から、forループと組み合わせたfor-selectループがよく使われれる。

for {
    select {
    case <- done:
        return
    case v := <-ch:
        fmt.Println(v)
    }
}

このパターンを使う場合はそこから抜ける方法を含めなければならない。
defaultのあるselectでは、前に進めるcaseがない場合にブロックは発生せずdefaultに記載された処理が実行される。

並行処理のベストプラクティス

APIに並行性は含めない

APIとして公開する型、関数、メソッドにチャネルおよびミューテックスを含めないようにする。
例外として並行性関連の関数と一緒に使うものはAPIの一部にチャネルを含むことになる。

ゴルーチンとforループ

ゴルーチンがゴルーチン外の変数に依存している場合(forループないでゴルーチンを使う場合など)、ゴルーチンで使われる値が必ずしも想定通りの値になるとは限らない。
ゴルーチンに対して決めた値を渡すには以下の二つの方法がある。
一つはループ内で値をシャドーイングする方法

a := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
ch := make(chan int, len(a))
for _, v := range a {
    v := v  // 外側のvをシャドーする
    go func() {
        ch <- v * 2
    }()
}

もう一つはゴルーチンの引数に値を渡す方法

a := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
ch := make(chan int, len(a))
for _, v := range a {
    go func(val int) { // valの値は下の引数vの値になる
        ch <- val * 2
    }(v)  // vの値を無名関数に引数で渡す
}

ゴルーチンの終了チェック

Goのランタイムは変数とは異なり、全く使われていないゴルーチンを検知できない。
ゴルーチンが終了しないとスケジューラが定期的にゴルーチンに時間を割振り全体の動作遅くなるゴルーチンリークが起こってしまう。
for-rangeでチャネルを処理している途中でfor文を終了するようなロジックの場合、for-rangeを記載している関数が終了するまでゴルーチンは終了せずゴルーチンリークになってしまう。

doneチャネルパターン

空の構造体のチャネル(done)を用意し、ゴルーチンの実行の際の引数として渡す。
ゴルーチン内の処理ではselectによってdoneの場合とメインの処理を記載する。
ゴルーチン呼び出し側でメインの処理からチャネルからの値を取得した後にdoneチャネルを終了することで呼び出しているゴルーチンが終了する。

キャンセレーション関数を用いたゴルーチンの終了

チャネルを返却する関数からリソースの後始末を行うクロージャを返すようなパターンもある。
doneチャネルをゴルーチンを使用する関数内で用意し、そのチャネルをクローズする関数も用意する。
返却値にチャネルとその関数を返すようにすることで、メインの処理からキャンセル関数を呼び出すことによるゴルーチンの終了ができる。

バッファ付きチャネルの使いどころ

起動したゴルーチンの数がわかっているため、「起動するゴルーチンの数を制限したい場合」や「バッファに入ったものの処理に制限をかけたい場合」に有用。

バックプレッシャ

空の構造体のチャネルを持つ型を用意する。
メソッドとして特定の数のチャネルを生成する関数とそれを消費して実際の処理を行う関数を用意する。
実際の処理を行う関数の引数として処理したい関数を渡し、selectを用いてチャネルがある場合は処理したい処理を行い空の構造体のチャネルをつかするcase,defaultにエラーを返すようにしておく。
これを使用することでチャネルに余裕がある時のみ処理を行うような制御ができる。

selectにおけるcaseの無効化

複数のチャネルをselectによって処理するときにそのチャネルがクローズしているか確認する必要がある。
そうしないとゼロ値の処理無駄な時間を消費してしまう。
対策として、カンマokイディオムでチャネルを取得し、クローズされている場合はそのチャネルにnilを代入し、continueを行う。
こうすることで一度実行されたcaseのチャネルから値を読み取っても、nilは値を返さないためそのcaseは実行されないことになる。

タイムアウト

以下のようにcaseを設定することで並行処理の実行時間を管理できる。

func timeLimit() (int, error) {
	var result int
	var err error
	done := make(chan struct{})
	go func() {
		result, err = doSomeWork()
		close(done)
	}()
	
	select {  // doneが閉じられるか
	case <-done:
		return result, err
	case <-time.After(2 * time.Second): // 2秒経過してしまうとこちら
		return 0, errors.New("タイムアウトしました")
	}
}

WaitGroupの利用

1つのゴルーチンが複数のゴルーチンの終了を待つ場合、doneチャネルパターンは使用できない。
その場合やsyncパッケージのWaitGroupを使う必要がある。
使用イメージは以下の通り。

func main() { //liststart1
	var wg sync.WaitGroup // 初期化不要
	wg.Add(3)

	go func() {
		defer wg.Done()
		doThing1()
	}()

	go func() {
		defer wg.Done()
		doThing2()
	}()

	go func() {
		defer wg.Done()
		doThing3()
	}()

	wg.Wait()
}

WaitGroupは宣言のみで初期化する必要はない。ゼロ値を使用する。
WaitGroupには、以下のメソッドがある。

  • Add
    終了を待つゴルーチン数のカウンタを指定した数だけ増やす
  • Done
    カウンタのデクリメント
  • Wait
    カウンタがゼロになるまでそのゴルーチンをポーズする

WaitGroupを明示的に渡さないのはクロージャを使って同じインスタンスを参照するようにするため。
またAPIに並行性を含めないようにするのも明示的に渡さない理由。
WaitGroupを使用するのは、処理を行う「ワーカー」となるゴルーチンが全て終了後、クリーンアップするものがある時のみ使用するべき。

コードを一度だけ実行

遅延読み込みを行いたい場合、必要なときに1度だけ特定の処理を行うことができるsyncパッケージのOnceという型がある。
WaitGroupと同様に宣言のみで初期化する必要はない。ゼロ値を使用し、インスタンスのコピーを渡さないようにする。
sync.Onceのインスタン生成は、関数内では行わない。
once.Doにクロージャを渡して実行する。Doメソッドはクロージャに関係なく一度のみ実行される。

チャネルではなくミューテックスを使うべきとき

複数のゴルーチンが共有されたデータの書き込み、読み込みは行うが、値の処理はしないケースはmutexを使用するべき。

mrmsmrms

11章 標準ライブラリ

入出力