『初めてのGo言語』を読む
1章 Go環境のセットアップ
- Goはバージョン管理ツール使わないらしい
$ brew install go
$ go version
go version go1.22.2 darwin/arm64
$ which go
/opt/homebrew/bin/go
$ go env GOPATH
/Users/yoshinominako/go
- Goはコンパイル言語
go run
-
go run [ファイル名]
でプログラム実行-
go run
でバイナリファイルがビルドされ一時ディレクトリに置かれる - そのファイルを実行する
- プログラムの実行後、ファイルが削除される
-
go run
でインタプリタ言語のスクリプトのように扱うことができる
-
-
go build
-
go build [ファイル名]
で実行形式のバイナリファイルを作成
$ go build hello.go
# helloという実行形式ファイルが同じディレクトリに生成される
$ ./hello
# helloの実行結果が表示される
go mod
- モジュールの作成
-
go/mod
ファイルがあるとファイルを指定しなくてもそのディレクトリに置かれた.go
ファイルを解析してコンパイルしてビルドしてくれる
-
$ mkdir hello-world
$ cd hello-world/
$ go mod init hello-world #モジュールの初期化 go.modというファイルが作られる
go: creating new go.mod: module hello-world
$ cp ../hello.go .
$ go mod tidy # ソースファイルを解析してサードパーティのライブラリのダウンロードを行う。tidy=きちんとした
$ go build
$ ./hello-world
Hello, everyone!
go install
- Goのプログラムはコンパイルされたバイナリの実行形式が配布されている場合もあるが、ソースが公開されていて、それを
go install
コマンドでインストールできる場合もある-
$GOPATH/bin
にソースコードがダウンロードされる
-
$ go install github.com/rakyll/hey@latest
コードのフォーマット
-
go fmt
- コードを標準の形式にフォーマットしてくれる
-
goimports
-
go fmt
の強化版 - import文をクリーンにしてくれる
-
-
セミコロン挿入規則
- Goの各文の終わりに
;
が必要 - Goのコンパイラが規則に従って自動的に挿入する
- この規則のために関数定義の最初の
{
は関数名と同じ行にないと構文エラーになる
- Goの各文の終わりに
lintとvet
-
staticcheck
- リンター
-
go vet
- 構文的には間違いがないものの、期待通りに動かないコードなどを検知してくれる
- メソッドに渡す引数の数を間違えているなど
- 構文的には間違いがないものの、期待通りに動かないコードなどを検知してくれる
-
golangci-lint
- 複数のツールを一緒に実行できる
-
staticcheck
、go vet
、その他さまざまなコードチェックツールを合体 - デフォルトでは10種類
-
- 複数のツールを一緒に実行できる
- まずは
go vet
をビルドプロセスに組み込み、コードのレビュープロセスのstaticcheck
を組み込むことを推奨。慣れてきたところでgolangci-lint
を使って調整。
- Goを使ってプロダクトを作る時、Makefileを使ってビルドを指定することが多い
コンパイルとビルドの違い
コンパイルはソースコードを機械語に変換する作業だけを指すのに対し、ビルドはコンパイルに加えて、リンクやテスト、実行ファイルの生成などを含む、より広範な作業を指す。
- Goはランタイム(実行系)に依存しないコンパイラ言語なので、開発環境を変更しても今まで動いていたプログラムが動かなくなる心配はない
- ランタイムに依存しないコンパイラ言語
- Go、C/C++、Rust
- ランタイムが必要な言語(Java以外はインタプリタ言語)
- Java、Python、Ruby、JavaScript
- ランタイムに依存しないコンパイラ言語
- 半年ごとにメジャーアップデートされる
- 複数バージョンのGoをインストールしたい場合は「Managing Go installations」を参照
2章 基本型と宣言
- 宣言されたが値が割り当てられていない変数に対してはデフォルトのゼロ値が割り当てられる
- Go言語のリテラル(=数値や文字、文字列などを直に示したもの)
- 整数リテラル
-
123
、0b1100
-
- 浮動小数点リテラル
-
3.14
、6.03e23
-
- runeリテラル
- 文字を表す
- シングルクォートで囲む
-
'a'
、'\n'
- 文字列リテラル
- 解釈済み文字列リテラル
- ダブルクォートで囲む
- エスケープされていない
\
と"
が使えない
- 生の文字列リテラル
- バッククォートで囲む
-
\
と"
を使いたいとき
- 解釈済み文字列リテラル
- リテラルは型がないので、互換性を持つ任意の変数に代入できる
- 整数リテラルを浮動小数点数型の変数に代入するなど
- 整数リテラル
- 論理型
bool
- ゼロ値は
false
- ゼロ値は
- 数値型
- ゼロ値は
0
- 整数型だったら
int
- 浮動小数点数型だったら
float64
- あくまでも近似値なので誤差があっても許容される場合に使う
- 複素数型はめったに使われない
- ゼロ値は
- 文字列型
- ゼロ値は
""
- Goの文字列はイミュータブル(変更不可)
- runeリテラルのデフォルトの型は
rune
- 文字列リテラルのデフォルトの型は
string
- ゼロ値は
- Goは暗黙的型変換を行わない。必ず明示的に行う必要がある
- Goは簡潔さよりも明快さと可読性に重きを置いている
- 代わりに冗長になりがち
- Goは簡潔さよりも明快さと可読性に重きを置いている
変数の宣言
var x int = 10
var x = 10 // 整数リテラルのデフォルトの型はintなので、型指定を省略できる
var x int // xにはゼロ値が代入される
var x, y = 10, "hello" // 型の異なる変数を同時に宣言することもできる
var (
x int
y = 20
) // 複数の変数の宣言をリストにまとめることができる
x := 10 // 関数内ではvarの代わりに:=が使える。パッケージレベルでは使えない
- 変数宣言の形式の選び方
- 変数をゼロ値に初期化するときは
var x int
- リテラルを変数に代入するとき、そのリテラルのデフォルト型と異なる型を割り当てたいときは
var x byte = 20
- それ以外は
:=
- 変数をゼロ値に初期化するときは
- パッケージレベルにおいては基本的にイミュータブルな変数のみを宣言すべき
定数
- Goの定数(
const
)はリテラルに名前をつけるためのもの - パッケージレベルあるいは関数内で宣言できる
- 定数には型があるものと型がないものがある
- 型のない定数はリテラルと同じように動作する
-
const x = 10
と型なしで宣言し、あとからx
をint
型やfloat64
型の変数に代入するケースなど
-
- 型のない定数はリテラルと同じように動作する
- 宣言されたローカル変数はすべて使われないとコンパイルエラーになる
- パッケージレベルの変数は使われなくてもエラーにならない
- 変数および定数の名前はキャメルケースを使う
3章 合成型
配列
- 配列が直接使われることはあまりない
- Goでは配列の長さを配列の型の一部としてみなしている
- 配列の型はコンパイル時に決定できなければならない
- 配列が使われるのは事前にサイズがわかる場合だけ
- Goでは配列の長さを配列の型の一部としてみなしている
- 配列同士は比較可能
var x [3]int // {0, 0, 0}
var x = [3]int{10, 20, 30} // {10, 20, 30}は配列リテラル
var x = [...]int{10, 20, 30} // 配列リテラルを使うときは長さを表す整数の代わりに...が使える
var x = [12]int{1, 5: 4, 6, 10: 100, 15} // 途中に空きがある配列
var x [2][3]int // 2次元の配列。長さ2の配列で、その型は長さ3の整数配列
x[0] = 10 // 要素の代入
len(x) // 配列の長さ
スライス
- ざっくり言うと「可変長の配列」
- 宣言時に長さを指定しない
- スライスのゼロ値は
nil
-
nil
は「値がない」ことを示す識別子
-
- スライス同士は比較できない
var x = []int{10, 20, 30} // []だけを書くとスライスになる
var x [][]int // 2次元のスライス
var x []int // xはスライスのゼロ値、すなわちnilが初期値になる
-
len
- スライスの長さ
-
nil
スライスをlen
に渡すと0
を返す
-
append
- スライスの要素を増やす
var x []int
x = append(x, 10) // 引数として渡されているのはxのコピー。戻された結果を必ずxに代入しなければならない
x = append(x, 5, 6, 7) // 複数の値の追加
y := []int{1, 2, 3}
x = append(x, y...) // 演算子...を使って、xの後にyの全要素を追加
-
cap
- スライスのキャパシティ
-
len
の値がキャパシティに達した状態でさらに値を追加しようとするとGoのランタイムがより大きなキャパシティをもつスライスの領域を確保する
- Goのランタイム
- メモリの確保、ガベージコレクションなど
- すべてのGoのバイナリファイルにコンパイル時に組み込まれる
-
append
によってスライスが大きくなってくると、コピーのために時間がかかるようになる- Goのランタイムはスライスのキャパシティを増やす際にはある程度の余裕をもってメモリを確保する
-
make
- 型、長さ、オプションでキャパシティを指定してスライスを作る
x := make([]int, 5) // 長さ5、キャパシティ5のintのスライス
x := make([]int, 5, 10) // 長さ5、キャパシティ10のintのスライス
- スライスのスライス(サブスライス)
- 親スライスとサブスライスはメモリを共有している
- サブスライスのキャパシティは、親スライスのキャパシティからオフセット分を引いた値
- オフセット=サブスライスの先頭要素が親スライスの何番目の要素かを表す値
- 親スライスで使われていなかったキャパシティもサブスライスと共有される
- サブスライスに
append
すると親スライスにも影響する- サブスライスで
append
を使わないようにするか、フルスライス式を用いてappend
で上書きが生じないようにする
- サブスライスで
x := []int{1, 2, 3, 4}
y := x[:2] // [1 2]
z := x[1:] // [2, 3, 4]
x := make([]int, 0, 5)
x = append(x, 1, 2, 3, 4) // [1 2 3 4] cap5
y := x[:2:2] // フルスライス式 [1 2] cap2
z := x[2:4:4] // [3 4] cap2
fmt.Println(cap(x), cap(y), cap(z))
y = append(y, 30, 40, 50) // キャパシティを超えるため、新しいスライスが作られる
x = append(x, 60)
z = append(z, 70)
fmt.Println("x:", x, "y:", y, "z:", z)
// x: [1 2 30 4 60] y: [1 2 30 40 50] z: [3 4 70]
x := [...]int{1, 2, 3, 4, 5} // 配列
y := x[:2] // スライス
- オリジナルとメモリを共有しない独立したスライスは
make
とcopy
で作れる-
copy
が引数として取れるのはスライスだけ- 配列のスライス表現を使ってスライスから配列、配列からスライスへのコピーもできる
- スライスは実際には配列へのポインタ、長さ、キャパシティの3つの要素で構成される構造体のようなもの。スライスを関数に渡すと、スライスの値(=配列へのポインタ)がコピーして渡される
- 配列のスライス表現を使ってスライスから配列、配列からスライスへのコピーもできる
-
x := int{1, 2, 3, 4}
y := make([]int, 4)
num := copy(y, x) // xからyにコピーする。numにはコピーされた要素数が入る
x := []int{1, 2, 3, 4} // xはスライス
d := [4]int{5, 6, 7, 8} // dは配列
copy(d[:], x) // xからd[:]にコピー。d[:]とdはメモリを共有しているため、dにも反映される
文字列
- 文字列を表現するのにバイト列が使われている
- 文字列に対してはスライスやインデックスを活用するよりも、標準ライブラリのパッケージ
strings
やunicode/utf8
の関数を使うのがよい
マップ
- 順番が重要なときはスライス、順番が重要でないときはマップを使う
- Go言語のマップはハッシュマップ
- ハッシュマップ以外のマップはツリーマップ、連想リストとか
var nilMap map[string]int // 値が割り当てられていないのでマップのゼロ値nilを持つ
fmt.Println(nilMap == nil) // true
// nilMap["abc"] = 3 とするとパニックになる
totalMins := map[string]int{} // 空のマップが割り当てられる
fmt.Println(totalMins == nil) // false
totalMins["abc"] = 3 // 読み書き可能
ages := make(map[string]int, 10)
- 値が設定されていないキーの値を取り出そうとするとマップの値の型のゼロ値が返される
- カンマokイディオム(comma ok idiom)
m := map[string]int{
"hello": 5,
"world": 0,
}
v, ok := m["hello"] // 5 true
v, ok := m["goodbye"] // 0 false
// マップからの削除
delete(m, "hello") // deleteは値を戻さない
- マップはセット(集合。要素に重複がない。順序関係なし)の昨日を部分的にシミュレートできる
- セットは
struct
(構造体)を使っても実装できる
- セットは
構造体
- マップでは特定のキーだけを含むような限定されたマップの定義ができない
type person struct {
name string
age int
pet string
}
var fred person // 各フィールドがそのフィールドの型のゼロ値をもったものになる
bob := person{} // 上と同じ
- 無名構造体
- 下記の2つのケースでよく使われる
- 外部データを構造体に変換したり、逆に構造体を外部データ(JSONなど)に変換する
- アンマーシャリング、マーシャリング
- テスト
- 外部データを構造体に変換したり、逆に構造体を外部データ(JSONなど)に変換する
- 下記の2つのケースでよく使われる
var person struct { // 変数personを無名の構造体としてゼロ値で初期化される
name string
age int
pet string
}
person.name = "ボブ"
person.age = 50
person.pet = "dog"
pet := struct {
name string
kind string
}{
name: "ポチ",
kind: "dog",
}
- 2つの構造体変数を比較する場合、少なくとも片方に無名構造体のフィールドがあると、型変換なしで比較できる場合がある
4章 ブロック、シャドーイング、制御構造
ブロック
- 内側のブロックで同じ名前の識別子を定義すると、外側で定義された識別子がシャドーイングされてしまう
func main() {
x := 10
if x > 5 {
fmt.Println(x) // 10
x := 5 // xが上書きされる
fmt.Println(x) // 5
}
fmt.Println(x) // 10
}
- シャドーイングの検知
shadow
というリンターで(ある程度は)できる
if
- 条件を(...)で囲まない
-
if
とelse
の両方のブロックで有効な変数を定義できる
func main() {
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)
}
}
for
- 4種類のfor文がある
- 「初期設定」「条件」「再設定」からなる標準形式
- 条件部分のみを指定するもの
- 無限ループ
-
for-range
を使うもの
for i := 0; i < 10; i++ { // 標準形式
fmt.Println(i)
}
// 条件のみのfor文(=while文)
i := 10
for i < 100 {
fmt.Println(i)
i = i * 2
}
for { // 無限ループ
fmt.Println("Hello")
}
- Go言語はキーワード(予約語)が25個と少ない
- if、for、returnとか
// breakで無限ループから抜け出す
for {
// 処理
if !ループ継続の条件 {
break
}
}
// Goではifの本体を短くすること、できるだけコードのネストを深くしないことが推奨される
// continueに到達するとループ内のそれ以降の処理をスキップする
for i := 1; i <= 100; i++ {
if i%3 == 0 && i %5 == 0 {
fmt.Println(i, "3でも5でも割り切れる")
continue
}
if i%3 == 0 {
fmt.Println(i, "3で割り切れる")
continue
}
if i%5 == 0 {
fmt.Println(i, "5で割り切れる")
continue
}
fmt.Println(i)
}
- マップに対するfor-rangeイテレーションの順番は毎回異なる
- Hash DoS攻撃対策
- fmt.Printlnによる出力は毎回一緒
evenVals = []int{2, 4, 6, 8}
for i, v := range evenVals {
fmt.Prntln(i, v) // iはindex、vはvalue
}
// indexが不要な場合
// 帰って来た値を無視したい場合_を使う
for _, v := range evenVals {
fmt.Println(v)
}
switch
- Goの
switch
はデフォルトでフォールスルーしない- ある
case
の処理が終わると、そこでswitch
を抜ける
- ある
-
switch
でもbreak
は使えるが、break
を使いたくなった場合はリファクタリングを検討したほうがよい
words := []string{"hi", "salutations", "hello"}
for _, word := range words {
switch wordLen := len(word); {
case wordLen < 5;
fmt.Println(word, "は短い単語です")
case wordLen > 10;
fmt.Println(word, "は長い単語です")
default:
fmt.Println(word, "はちょうどよい長さの単語です")
}
}
- Goにも
goto
はあるがなるべく使わないほうがいい
5章 関数
- Goの関数には「名前付き引数」と「オプション引数」がない
- 可変長引数はある
- 可変長引数に対応する変数として関数内で作成されるのは指定された型のスライス
// valsには値をいくつ渡してもいいし、渡さなくてもいい
func addTo(base int, vals ...int) []int {
out := make([]int, 0, len(vals))
fot _, v := range vals {
out = append(out, base+v)
}
}
func main() {
fmt.Println(add(3)) // []
fmt.Println(add(3, 2) // [5]
fmt.Println(add(3, 1, 2, 3, 4) // [4, 5, 6, 7]
}
- Goでは宣言した変数は必ず使わなければならないので、使わない戻り値は
_
に代入する- 戻り値すべてを使わない場合は代入文自体を書かない
- 名前付き戻り値のデメリット
- シャドーイングが起きる可能性がある
- 名前付き戻り値を設定しても、return文で別の値を返していたらそちらが優先される
- 名前付き戻り値を使うとブランクreturnが使えるが、ブランクreturnは使わないほうがよい
- ブランクreturnを使うと名前付き戻り値に最後に代入された値が戻されるが、開発者がその値を追わなければならなくなるため
関数のシグネチャとタイプ(型)の違い
Go言語の関数のシグネチャとタイプの違いについて、提供された情報を基に説明します。
関数のシグネチャは、関数の入力パラメータの型と戻り値の型を定義したものです。一方、関数のタイプは、関数のシグネチャに加えて、関数の名前も含みます。[1]
例えば、以下のような関数があるとします:
func Max(x, y float64) float64
この関数 Max
のシグネチャは func(float64, float64) float64
です。これは、2つの float64
型の引数を取り、float64
型の値を返すことを示しています。[1]
一方、この関数のタイプは func Max(float64, float64) float64
です。これは、関数の名前 Max
とそのシグネチャを組み合わせたものです。[1]
関数のタイプは、変数に関数を代入したり、関数を引数として渡したりする際に使用されます。例えば:
var mathFunc func(float64, float64) float64
mathFunc = Max
ここでは、mathFunc
という変数が func(float64, float64) float64
というタイプで宣言され、Max
関数が代入されています。[1]
このように、Go言語では関数のシグネチャとタイプを区別しています。シグネチャは入力と出力の型のみを定義するのに対し、タイプは関数の名前も含めて定義されます。関数のタイプは、関数を値として扱う際に重要な役割を果たします。
Citations:
[1] https://code.visualstudio.com/docs/languages/go
[2] https://www.freecodecamp.org/news/what-is-go-programming-language/
[3] https://www.geeksforgeeks.org/c-functions/
[4] https://sentry.io/answers/java-pass-by-reference-or-value/
[5] https://www.youtube.com/watch?v=9Oq5iBHFajE
[6] https://www.mail.com/blog/posts/cc-and-bcc/5/
[7] https://reactrouter.com/en/main/hooks/use-navigate
[8] https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/
- 無名関数は
defer
文、ゴルーチンの起動でよく使われる
func main() {
for i := 0; i < 5; i++ {
func(j int) {
fmt.Println("無名関数の中で", j, "を出力")
}(i)
}
}
クロージャ
- 関数f内で定義された関数gは、外側の関数f内で定義された変数にアクセスしたり変更したりできる
- このような関数gをクロージャと呼ぶ
- クロージャの機能
- 関数のスコープを宣言する
- 関数fからしか呼び出されない関数を「隠して」おくことで、パッケージレベルでの名前の衝突を防げる
- クロージャは別の関数に渡されたり、関数から返されたりすることで、関数の中で定義された変数をその環境ごと包み込んで持ち出して、関数の外で使えるようになる
- 関数のスコープを宣言する
GoのクロージャとJavaScriptの類似点
はい、GoのクロージャとJavaScriptのラムダ関数には類似点があります。
Goのクロージャ:
- 無名関数の一種で、関数の外で宣言された変数を参照できる。
- 関数が終了した後も、参照している変数の値を保持し続けられる。
- 関数の外にあるスコープの変数にアクセスできる。[5][12]
JavaScriptのラムダ関数(アロー関数):
- 無名関数を簡潔に書く構文。function キーワードの代わりに => を使う。
- 関数の外で宣言された変数を参照でき、クロージャを形成できる。
- thisの値を関数の外側のスコープから継承する。[1][15]
主な共通点:
- どちらも無名関数の一種。
- 関数の外で宣言された変数を参照でき、クロージャを形成できる。
- コードをシンプルかつ簡潔に書ける。
主な相違点:
- 構文の違い。Goは func() {...} 、JSは () => {...}
- Goのクロージャは明示的にreturnが必要。JSのラムダ式は暗黙のreturnがある。
- thisの扱いが異なる。Goにはthisの概念がない。
このように、Goのクロージャと、JavaScriptのラムダ関数は、クロージャを形成できる無名関数という点で類似していますが、言語の特性による構文や動作の違いがあります。どちらも関数型プログラミングを実現する強力な機能といえます。
Citations:
[1] https://www.vinta.com.br/blog/javascript-lambda-and-arrow-functions
[2] https://www.calhoun.io/5-useful-ways-to-use-closures-in-go/
[3] https://www.geeksforgeeks.org/lambda-expressions-in-javascript/
[4] https://joshdata.me/lambda-expressions.html
[5] https://www.developer.com/languages/closure-golang/
[6] https://www.scaler.com/topics/golang/closures-in-golang/
[7] https://www.programiz.com/golang/closure
[8] https://stackoverflow.com/questions/76442071/why-does-javascript-code-usually-use-lambdas-instead-of-named-functions
[9] https://www.bogotobogo.com/GoLang/GoLang_Closures_Anonymous_Functions.php
[10] https://docs.aws.amazon.com/lambda/latest/dg/lambda-nodejs.html
[11] https://gobyexample.com/closures
[12] https://www.geeksforgeeks.org/closures-in-golang/
[13] https://betterprogramming.pub/closures-made-simple-with-golang-69db3017cd7b?gi=db1479ab1d15
[14] https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html
[15] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
[16] https://hevodata.com/learn/javascript-lambda-function/
defer
- クロージャの実行を遅延させる
- defer=遅らせる
- LIFO(last-in-first-out:後入れ先出し)で実行する
- return文の後で実行される
Goは値渡し
-
int
、string
、構造体などの値型は関数に渡すと値がコピーされるため、関数内で変更しても元の値は変わらない - スライス、マップ、チャネルなどの参照型は内部的にポインタを使用しているため、値渡しでも実質的に参照渡しと同じ効果がある
- マップは内部的にポインタ、長さ、容量を持つ構造体として実装されている
- 関数にマップを渡す際、マップの構造体自体がコピーされるが、内部のポインタは同じアドレスを指している
6章 ポインタ
x := 10
var pointerToX *int // ポインタ型
pointerToX = &x // &はアドレス演算子
z := 5 + *pointerToX // *は間接参照のための演算子
- 基本型のリテラル(数、ブール値、文字列)や定数の前に
&
をつけることはできない- これらはプログラムの実行時にメモリ上に存在するのではなく、コンパイル時に直接機械語に変換されているから
- フィールドに基本型へのポインタを持つ構造体に、基本型のリテラルや定数を代入したいときは、リテラルや定数を一度変数に移して、その変数のアドレスを構造体のフィールドに代入すればよい
- ポインタはミュータブル(変更可能)の印
func update(px *int) {
*px = 20 // pxが参照するもの、つまり呼び出し元のxに20を代入する
}
func main() {
x := 10
update(&x)
fmt.Println(x) // 20
}
- ポインタ渡しは最後の手段
- 関数にポインタを渡して構造体の中身を埋めるのではなく、構造体のインスタンスを作成して返すようにすべき
- 変数の変更にポインタ引数を使わなければならないのは、その変数がインタフェースのときだけ
- JSONを扱う場合にこのパターンが出現する
- 変数の変更にポインタ引数を使わなければならないのは、その変数がインタフェースのときだけ
- 関数から返す値もポインタではなく値として返すべき
- 関数にポインタを渡して構造体の中身を埋めるのではなく、構造体のインスタンスを作成して返すようにすべき
// NG
func MakeFoo(f * Foo) error {
f.Field1 = "val"
f.Field2 = 20
return nil
}
// GOOD
func MakeFoo() (Foo, error) {
f := Foo{
Field1: "val",
Field2 = 20,
}
return f, nil
}
- ポインタ渡しのパフォーマンス
- 関数へ値を渡すとき
- ポインタを関数に渡すのにかかる時間は1ナノ秒
- 10MBのデータを関数に渡すのにかかる時間は1ミリ秒
- 関数から値が戻るとき
- 1MBより小さいデータの場合、ポインタ型を返すほうが遅い
- 1MBを超えるとパフォーマンスが逆転する
- どれも非常に短い時間なので、ほとんどの場合ポインタを使うか値を使うかの違いはプログラムのパフォーマンスに影響を与えない
- 関数が何MBものデータをやり取りする場合は、データがイミュータブル(変更不可)であってもポインタの使用を検討してよい
- 関数へ値を渡すとき
- 「ゼロ値」を設定されている変数やフィールドと「値なし」の変数やフィールドを区別するためにポインタを使用できる
- マップは構造体へのポインタとして実装されている
- マップを入力用のパラメータに使ったり、値を返すのに使うべきではない
- 構造体を使うべき
- マップを入力用のパラメータに使ったり、値を返すのに使うべきではない
- スライスはサイズを表すint型フィールド、キャパシティを表すint型フィールド、メモリブロックを参照するポインタで構成されている
- 関数内でスライスの値を変更したとき、元のスライスのサイズ内であれば、元のスライスからも変更された値がわかるが、サイズ外であればわからない
- 関数内でスライスのサイズを変更しても、元のスライスには反映されない
- スライスは再利用可能なバッファとして使える
file, err := os.Open(fileName)
if err != nil {
return err
}
defer file.Close()
// 100バイトのバッファを作成し、ループを回るたびに次のブロックをスライスにコピーしている
data := make([]byte, 100)
for {
count, err := file.Read(data)
if err != nil {
return err
}
if count == 0 {
return nil
}
process(data[:count]) // 読み込んだデータの処理
}
ガベージコレクタの負荷軽減策
- ガベージ
- どのポインタにも参照されていないデータ
- スタック
- コンパイラやOSが自動的に割り当てと解放を行う
- スタック上のメモリ割り当ては高速で単純
- スタックに何かを保存するためにはコンパイラ時にその大きさがわかっていなければならない
- Goの型はコンパイル時に必要なメモリサイズがわかるようになっているので、スタックに割り当てられる
- データをスタック上に保存できないとコンパイラが判断すると、コンパイラはデータをヒープ上に保存する(=エスケープという)
- ヒープ
- アプリケーションが割り当てと解放を動的に行う
- ガベージコレクタ(CやC++では人間)が管理するメモリ
- スタックよりも複雑
- ヒープ上のデータを参照しているスタック上のポインタがなくなれば、ヒープ上のデータはガベージになる
- Goが行うエスケープ解析は完璧ではない
- データをなるべくヒープに保存すべきでない理由
- ガベージコレクタの作業に時間がかかる
- Goのガベージコレクタは「処理効率の高さ」よりも「遅延時間の低減」を優先している
- ヒープのランダムなアクセスは、スタックのシーケンシャルなアクセスよりも遅い
- ガベージコレクタの作業に時間がかかる
- スタックに保存されるものをできるだけ多くして、ガベージコレクタの負担を減らすのがよい
- 構造体や基本型のスライスはメモリ上にシーケンシャルに並べられ、高速にアクセスできるようになっている
- ガベージを作らないためには、Goの「イディオム」を使えばよい
- 構造体や基本型のスライスはメモリ上にシーケンシャルに並べられ、高速にアクセスできるようになっている
7章 型、メソッド、インタフェース
- Goでは継承(inheritance)ではなく合成(composition)を推奨している
型
- Goの型は抽象型(=インタフェース)か具象型(=インタフェース以外)のいずれか
- 基底型
- 型Tが基本型(論理型、数値型、文字列型)あるいは型リテラルの場合、Tの基底型はT自身
-
type Operation func(x, y int) int
というユーザー定義型の基定型はfunc(x, y int) int
(関数の型リテラル)
-
- それ以外の場合、Tの宣言で参照している型がTの基底型になる
- 基本型か型リテラルにたどり着くまで参照をさかのぼる
- 型Tが基本型(論理型、数値型、文字列型)あるいは型リテラルの場合、Tの基底型はT自身
メソッド
- メソッドはユーザー定義の型に付随する関数
- メソッドは付随する型と同じパッケージで定義する
type Person struct {
LastName string
FirstName string
Age int
}
func (p Person) String() string { // (p.Person)がレシーバの指定
return fmt.Sprintf("%s %s:年齢%d歳", p.LastName, p.FirstName, p.Age)
}
- ポインタ型レシーバ
- メソッドがレシーバを変更するときに使わなければならない
- メソッドがnilを使う必要があるときに使わなければならない
- 値型レシーバ
- メソッドがレシーバを変更しないときに使うことができる
- その型にポインタレシーバのメソッドがひとつでもあれば、すべてのメソッドにポインタレシーバを使って形式を揃えるのが一般的
- ポインタ型レシーバのメソッドを値から呼び出すと、Goは自動的に値をポインタに変換してメソッドを呼ぶ
- 反対も同じ
- nilからもメソッドを呼び出せる(コンパイルエラーにならない)
- 値型レシーバのメソッドはパニックになる
- ポインタ型レシーバのメソッドはnilを処理できるように書かれていなければパニックになる
- 関数とメソッドの使い分け
- 関数:ロジックが入力引数のみに依存する場合
- メソッド:ロジックが起動時に設定された値や実行中に変更された値に依存する場合
type Score int
type HighScore Score
// ユーザー定義型に、その基底型(=下記の例のScore型やHighScore型の場合、基底型はint)と互換性のあるリテラルや定数を代入できる
var i int = 300
var s Score = 100
var hs HighScore = 200
// 変数を型が異なる変数へ代入することはできない
// s = i // コンパイルエラー
// hs = s // コンパイルエラー
// 型変換後の代入はOK
s = Score(i)
hs = HighScore(s)
// 基底が基本型のユーザー定義型であれば、基本型に対して使える演算子を利用できる
// hhsはHighScore型となる
hhs := hs + 20
- Goには列挙型(enum型)がない
-
iota
- 連続した整数値を一連の定数に割り当てられる
- デフォルトは0から
- 値に意味があるときは
iota
を使わないほうがいい
- 連続した整数値を一連の定数に割り当てられる
type MailCategory int
const (
Uncategorized MailCategory = iota // 0が代入される
Personal // 型がないときは、最初の定数の型が暗黙的に推論される。1が代入される
Spam // 2が代入される
Social // 3が代入される
)
- 構造体には別の構造体のフィールドを埋め込みできる
- 埋め込まれたフィールドのメソッドも使える
type Employee struct {
Name string
ID int
}
type Manager struct {
Employee // 埋め込みフィールド
Reports []Employee // 部下
}
func (e Employee) String() string {
return fmt.Sprintf("Name: %s, ID: %d", e.Name, e.ID)
}
func main() {
e := Employee{Name: "Tanaka", ID: 100}
fmt.Println(e.String())
m := Manager{
Employee: Employee{Name: "Suzuki", ID: 200},
Reports: []Employee{},
}
fmt.Println(m.String()) // 埋め込まれたフィールドのメソッドが使える
var e2 Employee
// e2 = m // コンパイルエラー
e2 = m.Employee
fmt.Println(e2.String())
}
インタフェース
- Goで唯一の抽象型(実装を提供しない型)
- メソッドの集合
- 型の集合
- インタフェースの名前は〜erで終わる
- Goのインタフェースが特別なのは「暗黙的」に実装されること
- 構造体のフィールドにインタフェース型のフィールドを宣言できる
- インタフェースを受け取り構造体を返すようにコードを書くべき
-
interface{}
はany
とも書ける- 使い道
- JSONファイルのような外部ソースから読み込まれた形式不明なデータを記憶したいとき
- スライスや配列、マップには収まらないようなデータを保持するとき
- しかしなるべく使わないほうがよい
- 使い道
型アサーションと型スイッチ
- 型アサーション
- assert=仮定
- インタフェース型の変数が特定の具象型を持っているか、その具象型が別のインタフェースを実装しているか調べる
- インタフェース型にのみ適用可能で、実行時にチェックされ、アサーションが満たされれば値が返る
- 一方型変換は、具象型にもインタフェース型にも適用され、コンパイル時にチェックされ、結果として型が変わる
- なるべくカンマokイディオムを使う
var i any
var mine MyInt = 20
i = mine
i2 := i.(MyInt) // iをMyInt型だと仮定してその値を取得する
fmt.Println(i2) // 20
i3, ok = i.(int) // カンマokイディオムで確認
if !ok {
fmt.Println("iの型が想定外です")
}
- 型アサーションを使うケース
- あるインタフェース型の変数が、別のインタフェースも実装しているかどうか調べたいとき
- デコレータパターンを使って実装しているときは検知できないという欠点がある
暗黙のインタフェースによる依存性注入
依存性注入のできていないコード
package main
import (
"database/sql"
"fmt"
"log"
)
type User struct {
ID int
Name string
}
type UserService struct {
db *sql.DB
}
func NewUserService() *UserService {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
return &UserService{db: db}
}
func (s *UserService) GetUser(id int) (*User, error) {
// dbを使ってユーザーを取得する処理
row := s.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)
user := &User{}
err := row.Scan(&user.ID, &user.Name)
if err != nil {
return nil, err
}
return user, nil
}
func main() {
userService := NewUserService()
defer userService.db.Close()
user, err := userService.GetUser(1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
}
依存性注入をしているコード
type UserRepository interface {
GetUser(id int) (*User, error)
}
type UserService struct {
userRepo UserRepository
}
func NewUserService(userRepo UserRepository) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.userRepo.GetUser(id)
}
type MySQLUserRepository struct {
db *sql.DB
}
func NewMySQLUserRepository(db *sql.DB) *MySQLUserRepository {
return &MySQLUserRepository{db: db}
}
func (r *MySQLUserRepository) GetUser(id int) (*User, error) {
// dbを使ってユーザーを取得する処理
}
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
userRepo := NewMySQLUserRepository(db)
userService := NewUserService(userRepo)
user, err := userService.GetUser(1)
if err != nil {
log.Fatal(err)
}
fmt.Println(user)
}
8章 エラー処理
- Goは関数からerror型を戻すことによってエラーを処理する
- 例外はない
func calcRemainderAndMod(numerator, denominator int) (int, int, error) {
if denominator == 0 {
// errors.Newで新しいエラーを作成
// エラーメッセージは小文字で始めてピリオドなし
// ほかの戻り値はそれぞれの型のゼロ値に
return 0, 0, errors.New("denominator is 0")
}
// エラーがないときはエラー変数をnilで戻す
return numerator / denominator, numerator % denominator, nil
}
func main() {
numerator, denominator := 20, 3
remainder, mod, err := calcRemainderAndMod(numerator, denominator)
// if文を使ってエラー変数がnil以外の値になっているかどうかチェック
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("%d÷%d: 商:%d, 余り:%d\n", numerator, denominator, remainder, mod)
}
- エラーを生成する方法
errors.New
-
fmt.Errorf
:fmt.Printf
のすべての動詞(%s
、%d
、%l
など)を使うことができる
センチネルエラー
- 特定のエラー条件を表すために事前に定義された共有のエラー変数
- センチネル=番兵、番人
- 慣習により、名前はErrで始まる
- エラーメッセージの文字列で比較している
- なるべく標準ライブラリにすでに存在するものを使うほうがよい
- 特定のエラーを示唆する状態になり、それ以上処理が継続できず、エラーの状態を説明するためにコンテキスト情報を使う必要がないのならば、センチネルエラーが正しい選択
エラーと値
- errorはインタフェースなので、ロギングやエラー処理のための付加的な情報を含む独自のエラーを定義できる
- カスタマイズされたエラーを使う際は、エラー変数をカスタマイズされたエラーの型で定義してはいけない
- 「エラーが起こらなかったときにnilが戻っていない」というバグにつながるため
- エラーが起こらなかったときに明示的にnilを戻す、あるいは変数をerror型で定義する必要がある
type Status int
const (
InvalidLogin Status = iota + 1
NotFound
)
type StatusErr struct {
Status Status
Message string
}
func (se StatusErr) Error() string {
return se.Message
}
func GenerateError(flag bool) error {
var genErr error // StatusErr型にしない
if flag {
genErr = StatusErr{
Status: NotFound,
}
}
return genErr
}
エラーのラップ
- エラーが戻されるときに、エラーを受け取った関数名や実行しようとした操作などのコンテキストを付け加えることを「エラーをラップする」という
- ラップされたエラーが連続しているもののことを「エラーチェーン」という
-
fmt.Errorf
でエラーをラップできる- エラーフォーマットの文字列の最後に
%w
を追加し、そのエラーがfmt.Errorf
に渡される最後お引数をラップするようにすることが慣習になっている
- エラーフォーマットの文字列の最後に
-
error.Unwrap
でエラーをアンラップする- 通常、
error.Unwrap
を直接呼ぶことはせず、error.Is
やerror.As
を使って特定のラップされたエラーを見つけるのが一般的
- 通常、
// %vは引数のerrorをそのまま文字列化する
err := fmt.Errorf("エラー発生: %v", err)
// %wは引数のerrorをラップする
// fmt.Errorfが返すerrorは、引数のerrorをUnwrapメソッドで取り出せるようになる
err := fmt.Errorf("エラー発生: %w", err)
IsとAs
-
error.Is
- 戻されたエラー、あるいはラップしたその他のエラーが特定のセンチネルエラーのインスタンスにマッチするとき
- デフォルトでは
==
を使って、ラップされた各エラーと指定されたエラーの比較を行う- 自分で定義したエラー型が比較可能な型でないときはメソッド
Is
を定義する- 独自の
Is
メソッドを定義することでパターンマッチなどもできる
- 独自の
- 自分で定義したエラー型が比較可能な型でないときはメソッド
func fileChecker(name string) error {
f, err := os.Open(name)
if err != nil {
return fmt.Errorf("in fileChecker: %w", err)
}
f.Close()
return nil
}
func main() {
err := fileChecker("not_here.txt")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("That file doesn't exist")
}
}
}
-
errors.As
- 戻されたエラー、あるいはそれがラップするエラーが特定の型にマッチするかをチェックできる
-
As
メソッドのオーバーライドの実装は少し厄介で、リフレクションが必要なので、一般的ではない状況でのみ行うべき
-
- 戻されたエラー、あるいはそれがラップするエラーが特定の型にマッチするかをチェックできる
type MyErr struct {
Codes []int
}
func (me MyErr) Error() string {
return fmt.Sprintf("codes: %v", me.Codes)
}
func main() {
err := AFunctionThatReturnsAnError()
var myErr MyErr
// 1つ目の引数に調査したいエラー変数、2つ目の引数に比較したいエラー型の変数のポインタを渡す
// 型がマッチした場合、マッチしたエラーが2つ目の引数に代入される
if errors.As(err, &myErr) {
fmt.Println(myErr.Code)
}
// 2つ目の引数はインタフェースへのポインタでもよい
var coder interface {
Code() int
}
if errors.As(err, &coder) {
fmt.Println(coder.Code())
}
}
- deferを使ってエラーのラップを共通化できる
パニックとリカバー
- Goのランタイムが次に何をすればよいのか判断できないとき、パニックが生成される
- パニックが起こると実行中の関数は即座に終了する
- その関数に付加されて
defer
されていたものが実行される - 以下同様に、mainに到達するまで行われる
- プログラムがメッセージとスタックトレースを表示して終了する
- パニックは危機的な状況でのみ使うようにし、
recover
を用いて静かに終了するようにする-
recover
を使って現況をログに書き出し、os.Exit(1)
を使って終了するのがもっとも安全
-
-
recover
が推奨されるのは、サードパーティ用のライブラリを作成しているとき- 公開APIの範囲を超えてパニックを伝搬させないように、
recover
を使ってパニックをエラーに変換する
- 公開APIの範囲を超えてパニックを伝搬させないように、
// 組み込みの関数panicには任意の型(通常は文字列)をひとつ指定する
func doPanic(msg string) {
panic(msg)
}
func main() {
doPanic(os.Args[0])
}
func div60(i int) {
defer func() {
// recoverはdeferの中で呼び出さなければならない
// パニックになると通常実行される部分は実行されずにdeferされた部分だけが実行される
if p := recover(); p != nil {
fmt.Println("Recovered:", p)
}
}()
fmt.Println(60 / i)
}
func main() {
for _, val := range []int{1, 2, 0, 6} {
div60(val)
}
}
9章 モジュールとパッケージ
リポジトリ、モジュール、パッケージ
- Goのライブラリは大きい方から「リポジトリ」「モジュール」「パッケージ」の3つを使って管理される
- 1リポジトリ ≒ 1モジュール
- 1つのリポジトリに複数のモジュールを含めることも可能だが、推奨されていない
- 標準ライブラリ以外のパッケージのコードを利用するときは、事前に自分のプロジェクトをモジュールとして宣言する必要がある
- すべてのモジュールはグローバルにユニークな識別子を持つ
モジュールとgo.modファイル
- モジュールのルートディレクトリに
go.mod
ファイルが必要-
go mod
コマンドを使って作成する
-
go mod init MODULE_PATH
-
MODULE_PATH
の例- 公開する場合:
github.com/<モジュール名>
- 公開しない場合:
<会社名>/mymodule
- 他のモジュール名と衝突しない名前を使うようにする
- 公開する場合:
パッケージの構築
-
import
文によって、他のパッケージでエクスポートされた識別子(定数、変数、型、関数、メソッド、構造体のフィールド)にアクセスできるようになる - 識別子をエクスポートするには、先頭文字を大文字にすること
- 構造体のフィールドについても同様
- エクスポートされる識別子は全てドキュメントに記載し、後方互換性を保つようにしなければならない
- インポートパスは「絶対パス」を使うことが推奨されている
-
go mod init MODULE_PATH
でgo.mod
を作成 -
go mod tidy
で外部のモジュールをダウンロード -
go run main.go
またはgo build
でビルドしてから実行 - パッケージ名はインポートパスではなくパッケージ節(パッケージファイルの1行目で宣言した名前)で決まる
- 一般的にはパッケージ名とパッケージを含むディレクトリ名は同じにするべき
- パッケージとディレクトリで異なる名前をつけるケースは、ディレクトリを使ったバージョニングをするときなど
- 一般的にはパッケージ名とパッケージを含むディレクトリ名は同じにするべき
- パッケージ名のオーバーライド
- 同じ名前の別のパッケージをインポートするとき
import (
crand "crypto/rand" // crandという名前でインポートする
"math/rand"
)
-
go doc パッケージ名
で指定されたパッケージとパッケージ内の識別子のリストを表示する -
go doc パッケージ名.識別子
でパッケージ内の特定の識別子のドキュメントを表示する -
internal
というパッケージ名にすると、内部パッケージの仕組みが使える。直接の親パッケージおよび兄弟の位置にあるパッケージからしかアクセスできない -
init
関数- 引数なしで値を返さない
init
という名前の関数を宣言しておくと、そのパッケージが他のパッケージから最初に参照されたときに、init
関数が自動的に実行される - 一つのパッケージ内で複数の
init
関数を宣言できるが、一つにしておくべき -
init
関数が使われているパッケージの読み込みにはブランクインポートが使われることがあるimport _ "github.com/lib/pq"
- 引数なしで値を返さない
- Goでは循環参照は許されていない
- 循環参照が発生した場合は、パッケージを統廃合して対応する
- リファクタリングを行うとき、後方互換性を保つためには、オリジナルの識別子を削除するのは避けて、別名を提供するのがよい
- 関数あるいはメソッドの場合は、オリジナルのものを呼び出す関数あるいはメソッドを宣言すればよい
- 定数に関しては、同じ型と値を持つ新しい名前の定数を宣言すればよい
- エクスポートされた型を名称変更あるいは移動したい場合は、エイリアスを使う必要がある
-
type T2 = T1
でT1にアクセスするのにT2が使えるようになる- エイリアスはオリジナルの型の変数に型変換なしで代入できる
- パッケージレベルの変数、構造体のフィールドはエイリアスを持てない
-
モジュール関連の操作
- go.modファイル:
- モジュール名(プロジェクトのルートパッケージのインポートパス)
- 必要とされる依存パッケージとそのバージョン制約
- 間接的な依存関係の置換やexclude設定
- Go言語のバージョン制約
module example.com/myproject
go 1.16
require (
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.1
)
exclude github.com/pkg/errors v0.8.0
- go.sumファイル:
- 依存パッケージの正確なバージョンとそのチェックサム
- go.modで指定された直接的、間接的な全ての依存パッケージ情報
- モジュールの整合性を保証し、再現可能なビルドを可能にする
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
- モジュールのバージョン
- コマンド
go list
でモジュールで利用可能なバージョンを確認できる -
go get
コマンドを使うと、依存関係にあるモジュールのバージョンを変更できる- コマンドを実行したワーキングディレクトリに対応するモジュールのみが影響を受ける
- コマンドを実行してから
go.mod
を見ると、バージョンの変更が確認できる- マイナーバージョン、パッチバージョンのアップデートは
go get
コマンドだけでOK - メジャーバージョンのアップデートは、import文に書くモジュールへのパスをvN(Nはメジャーバージョン)の形式に変更してから
go get -u
(現在のメジャーバージョン内の最新のマイナーバージョンとパッチバージョンにアップデートしたいとき)またはgo mod tidy
(バージョンアップデートはせず、依存関係の整理だけするとき)
- マイナーバージョン、パッチバージョンのアップデートは
- コマンド
- セマンティックバージョニング
- メジャーバージョン
- 後方互換性を保たない変更
2.0.0
- 後方互換性を保たない変更
- マイナーバージョン
- 後方互換性を保った新しい機能の追加
1.1.0
- 後方互換性を保った新しい機能の追加
- パッチバージョン
- バグフィックス
1.0.1
- バグフィックス
- メジャーバージョン
- ミニマルバージョン選択
- 複数のモジュールが同じモジュールに依存しているが、そのバージョンが異なるとき、Goはもっとも新しいバージョンだけを取得する
- ちなみにnpmは同じパッケージの複数のバージョンを取得している
- 複数のモジュールが同じモジュールに依存しているが、そのバージョンが異なるとき、Goはもっとも新しいバージョンだけを取得する
-
pk.go.dev
- Goモジュールのドキュメンテーションを集めるサイト
- モジュールを公開したいときは、GitHubなどのパブリックなVCS(Version Control System)にソースコードを置いておけばよい
- npmのような中央集権的なライブラリのリポジトリにモジュールをアップロードする必要はない
-
go get
コマンドを使うとき、デフォルトではソースコードリポジトリから直接コードをフェッチしない。代わりに、Googleが運営しているプロキシサーバにリクエストを送る- このサーバはパブリックなすべてのGoモジュールの全てのバージョンのコピーを持っている
- プロキシサーバに加えて、チェックサムデータベースも保守している
- サードパーティのライブラリに対するリクエストをGoogleに送りたくない場合
- 環境変数
GOPROXY
をdirect
に設定することで、プロキシ機能を無効にできる- リポジトリから削除されたバージョンに依存している場合は、アクセスできなくなる
- 独自のプロキシサーバを立てることもできる
- 環境変数
10章 並行処理
- 並行処理と並列処理は違う
- 並行処理(concurrency)=ゴルーチン
- 一つの処理を独立した複数のコンポーネントに分割し、コンポーネント間で安全にデータを共有しながら計算すること
- 並行性を持ったコードが並列に実行されるかどうかはハードウェアとアルゴリズムに依存する
- 並行性が増しても高速化するとは限らない
- 並列処理(concurrent processing)
- 複数のタスクやプロセスを同時に複数のCPUを使って実行すること
- 並行処理(concurrency)=ゴルーチン
- CSP(Communicating Sequential Process)
- 1978年
- Goの並行性のモデルとなった論文
- 並行性を利用したほうがよいか確かでない場合は、まずコードを逐次的に書き、並行性を利用した実装とパフォーマンスを比較するベンチマークを書いてみるべき
- プロセス
- 具体的なプログラムがコンピュータのOSによって実行されているもの
- OSはメモリなどをプロセスと関連付け、ほかのプロセスがそうしたリソースにアクセスできないようにする
- スレッド
- ひとつのプロセスは、ひとつ以上のスレッドからなる
- ひとつのプロセス内のひとつ以上のスレッドがリソースへのアクセスを共有する
- ひとつのCPUは同時にひとつあるいは複数のスレッドの命令を実行できる(コアの数による)
- ゴルーチン
- Goのランタイムによって管理される「軽い」スレッド
- 関数の前にキーワード
go
が書かれているとゴルーチンになる
チャネル
- チャネル
- ゴルーチンでは情報のやり取りにチャネルを使う
ch := make(chan int)
a := <-ch // チャネルからの読み込み
ch <- b // チャネルへの書き込み
// チャネルを引数に取るとき
// <-chan が受信専用チャネル、chan<- が送信専用チャネルを表す
func runThungsConcurrently(chIn <- chan int, chOut chan<- string) {
// 処理
}
- チャネルに書き込まれた値は一度だけ読み込むことができる
- ひとつのゴルーチンが同じチャネルに対して読み書き両方を行うのは一般的ではない
- デフォルトではチャネルはバッファリングされない
- バッファリングされていないチャネルでは、送信と受信が同期的に行われる
- ほとんどの場合、バッファリングされていないチャネルを使うべき
- バッファリングされたチャネルでは、指定されたバッファサイズ分の値を、受信側が受け取る前に保持することができる
- バッファリングされていないチャネルでは、送信と受信が同期的に行われる
// バッファリングされるチャネルの生成
// バッファのキャパシティを指定
ch := make(chan int, 10)
- for-rangeループを使ってチャネルの読み込みができる
for v:= range ch {
fmt.Println(v)
}
- チャネルのクローズ
- クローズ後に書き込もうとしたり、再びクローズしようとするとパニックになる
- チャネルのクローズの責任は書き込み側のゴルーチンにある
// チャネルのクローズ
close(ch)
// チャネルがクローズされたかどうかを検知する
v, ok := <- ch
select
- 複数のチャネルに対する読み込みあるいは書き込みの操作が可能になる
- ひとつの
case
に対して読み込みあるいは書き込みが可能な場合に、その操作とcase
の本体が実行される - 複数の
case
が読み込みあるいは書き込み可能な場合は、ランダムに実行される
- ひとつの
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から値をもらったが、値は無視した")
}
並行処理のベストプラクティスとパターン
- APIに並行性は含めない
- APIとして公開する型、関数、メソッドにチャネルおよびミューテックスを含めないようにする
- ゴルーチンとforループ
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) { // vを直接参照すると、forループに対応するvではなく、このゴルーチンが実行される時点でのvの値が使われる
ch <- val * 2
}(v)
}
for i := 0; i < len(a); i++ {
fmt.Println(<-ch, " ")
}
- ゴルーチンの終了チェック
- ゴルーチンとして実行される関数を起動する際は確実に終了するようにしなければならない
- ゴルーチンが終了しない場合、スケジューラは定期的にゴルーチンに時間を割り振るため、全体の動作が遅くなる=ゴルーチンリーク
- ゴルーチンとして実行される関数を起動する際は確実に終了するようにしなければならない
package main
import "fmt"
func countTo(max int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i <= max; i++ {
fmt.Printf("Sending: %d\n", i)
ch <- i
}
fmt.Printf("Closing channel\n")
close(ch)
}()
return ch
}
func doSomethingTakingLongTime() {}
func main() {
for i := range countTo(10) { // チャネルに値が送信されるとループが回る
fmt.Printf("Received: %d\n", i)
// 下記のようにループを途中で抜けると、doSomethingTakingLongTimeが実行されている間ずっと
// 無名関数のゴルーチンは終了せずにブロックし続けることになる
// if i > 5 {
// break
// }
}
fmt.Printf("Done\n")
doSomethingTakingLongTime()
}
- doneチャネルパターン
- ゴルーチンに対して処理を終了するべきときであるというシグナルを送る
func searchData(s string, searchers []func(string) []string) []string {
done := make(chan struct{})
resultChan := make(chan []string)
for _, searcher := range searchers {
go func(f func(string) []string) {
select {
case resultChan <- f(s): // f(s)の結果をresultChanチャネルに送信
fmt.Println("Sent result")
case <-done: // doneチャネルから受信
fmt.Println("Done")
}
}(searcher)
}
r := <-resultChan // 最初にresultChanチャネルに送られたf(s)の結果を受信
close(done) // doneチャネルをクローズ。起動中のゴルーチンの done の case が実行され、ゴルーチンが終了する
return r
}
- キャンセレーション関数を用いたゴルーチンの終了
- 処理をキャンセルするためのクロージャを生成する
- いつバッファ付きのチャネルを使うべきか
- バッファ付きのチャネルを正しく使うには、バッファがいっぱいになった場合にどう対処するかを記述しなければならない
- 起動した一群のゴルーチンからデータを集めたい場合、並行実行の程度を制限したい場合に有用
- バックプレッシャ
- システムが全体として効率よく動作するよう、同時リクエストの数を制限する
- selectにおけるcaseの無効化
- nilチャネルに対する読み書きはハングアップ(=永遠にブロック)することを利用する
// in, in2, doneはチャネル
for {
select {
case v, ok := <- in:
if !ok {
in = nil // このcaseは再度成功することはない
continue
}
// vの処理
case v, ok := <- in2:
if !ok {
in2 = nil
continue
}
// vの処理
case <- done:
return
}
}
- タイムアウト
func timeLimit() (int, error) {
var result int
var err error
done := make(chan struct{})
go func() {
result, err = doSomeWork()
close(done)
}()
select {
case <-done:
return result, err
case <- time.After(2 * time.Second): // 2秒経過
return 0, errors.New("タイムアウトしました")
}
}
- 標準ライブラリのパッケージ
sync
にあるWaitGroup
の利用- ひとつのゴルーチンが複数のゴルーチンの終了を待つとき
- ワーカーとなるゴルーチンがすべて終了したあとで、クリーンアップするものがあるときのみ用いるべき
func main() {
var wg sync.WaitGroup // sync.WaintGroupは明示的な初期化の必要はない
wg.Add(2) // 終了を待つゴルーチン数のカウンタを2増やす
go func() {
defer wg.Done() // カウンタをデクリメントする
doThing1()
}()
go func() {
defer wg.Done()
doThing2()
}()
wg.Wait() // カウンタがゼロになるまで、これを含むゴルーチンをポーズする
}
- コードを一度だけ実行
- 遅延読み込みしたい場合
-
sync.Once
で一度だけ特定の処理を実行できる
チャネルの代わりにミューテックスを使うべきとき
- mutex=mutual exclusionの短縮形
- mutexがデータ保護に使われる際、データは並行に実行されているプロセスのすべてによって共有されている
- mutexが使われるのは、複数のゴルーチンが共有されたデータを読み込んだり、単純な書き込みをしたりはするものの、その値の処理はしないというケース
- 例:複数人で行うゲームでインメモリのスコアボードを利用する
11章 標準ライブラリ
- Goは標準ライブラリが充実している
- アプリケーション構築に必要なものは「すぐに使えるようにすべて含める」
io
-
io.Reader
とio.Writer
は数あるGoのインタフェースの中でも特によく使われる-
io.Closer
、io.Seeker
- これら4つのインタフェースをさまざまに組み合わせたインタフェース(
io.ReadCloser
、io.WriteCloser
など)も定義されている- こうしたインタフェースを使って自分の関数がデータに関して期待しているものを指定する
-
type Reader interface {
Read(p []byte) (n int, err error) // 読み込み結果を引数のスライスに反映させ、読み込んだバイト数とエラーが返される
}
type Writer interface {
Write(p []byte) (n int, err error) // 引数として書き込まれるデータを渡し、書き込まれたバイト数とエラーが返される
}
time
- 時間は
time.Duration
で表現される
// 2時間30分45秒
d := 2 * time.Hour + 30 * time.Minute + 45 * time.Second // dの型はtime.Duration
- 時刻は
time.Time
で表され、タイムゾーンが付随する- 時刻の比較は
==
ではなく、タイムゾーンのチェックも行ってくれるEqual
を使う - MST(UTCより7時間遅い)の2006年1月2日正午3時4分5秒(
01/02 03:04:05PM '06-0700
)をどのように表したいかを示した文字列を用意することで、時刻の表示方法を指定する
- 時刻の比較は
- OSでは2種類の「時」を記憶している
- ウォールクロック
- 現在時刻
- ウォールクロックの進みは一定ではない(サマータイムなど)
- モノトニッククロック
- コンピュータが起動されてからの時間
- Goではタイマーがセットされたり、
time.Time
のインスタンスが生成されたりした場合、経過時間を計るためにモノトニックタイムを使っている
- ウォールクロック
encoding/json
- マーシャリング:Goのデータ型からJSONへの変換
- アンマーシャリング:JSONからGoのデータ型への変換
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"` // 構造体タグでデータを処理するための規則を指定する
Age int `json:"age"` // 他パッケージから参照できるように、フィールド名の先頭は大文字に
Email string `json:"email"`
}
func main() {
// json.Marshalの例
person := Person{
Name: "John Doe",
Age: 30,
Email: "john@example.com",
}
jsonData, err := json.Marshal(person)
if err != nil {
fmt.Println("JSON Marshal error:", err)
return
}
jsonString := string(jsonData)
fmt.Println("Marshaled JSON:", jsonString)
// json.Unmarshalの例
var unmarshaledPerson Person
err = json.Unmarshal(jsonData, &unmarshaledPerson)
if err != nil {
fmt.Println("JSON Unmarshal error:", err)
return
}
fmt.Println("Unmarshaled Person:", unmarshaledPerson)
}
-
json.Marshal
、json.UnMarshal
とio.Reader
、io.Writer
を組み合わせたjson.Decoder
とjson.Encoder
という型もある
net/http
- 2010年代に発表されたGoの標準ライブラリには、他の言語ディストリビューションがサードパーティの責任と考えていたものであるHTTP/2のクライアントとサーバが含まれている
- 標準ライブラリのリクエストルータ:
*http.ServeMux
- 標準ライブラリのミドルウェア:
http.Handler
のファンクションチェーンで対応 - サードパーティのミドルウェア:
alice
- サードパーティのリクエストルータ:
gorilla/mux
、chi
- どちらも
http.Handler
やhttp.HandleFunc
とともに動作するため、イディオム的である
- どちらも
- 標準ライブラリのリクエストルータ:
12章 コンテキスト
- Goではコンテキストという概念を使ってリクエストのメタデータにまつわる問題を解決している
- 処理のデッドライン(
time.Time
) - キャンセレーションのシグナル
- その他の処理に必要な値
- 処理のデッドライン(
- 関数の最初の引数としてコンテキストを明示的に渡す
func logic(ctx context, info string) (string, error) {
// 何らかの処理
return "", nil
}
data := "some messages"
ctx := context.Background() // 空の初期コンテキストを生成
result, err := logic(ctx, data)
- コンテキストがGoのAPIに追加されたのは
net/http
パッケージが作成されたずっと後 - HTTPサーバを記述する際には、コンテキストを取得し、ミドルウェアのレイヤーを経てトップレベルの
http.Handler
に渡す
func Middleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func (rw http.ResponseWriter, req *http.Request) {
// req.Context()でそのリクエストに関連するcontext.Contextを返す
ctx := context.req.Context()
req = req.WithContext(ctx) // コンテキストをラップする
// context.Contextを受け取り、古いリクエストの状態と渡されたcontext.Contextを組み合わせた新しいhttp.Requestを返す
handler.ServeHTTP(rw, req)
})
}
func handler(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
err := req.ParseForm()
if err != nil {
// エラー処理
return
}
data := req.FormValue("data")
result, err := logic(ctx, data)
if err != nil {
// エラー処理
return
}
rq.Write([]byte(result))
}