Go
Go の言語リファレンス。言語に依存しない考え方は別の階層に置いてあるので、 対応するものを先に示す。ここは「Go ではどう書くか」に集中する。
横断的な話題との対応
| この文書の節 | 言語をまたぐ記述 |
|---|---|
| エラー処理 | 例外条件の扱い — err を握り潰すと何が起きるか。フェイルオープンとフェイルクローズド |
| 並行処理 | 分散システム — 同時実行数を絞るのは、goroutine の数を制限する話と同じ問題 |
| コンテキスト | 分散システム — 期限を呼び出し先へ引き継ぐ。context.WithTimeout はその実装そのもの |
| テスト | テスト — なぜ書くか、何をテストするか、規模の分け方 |
| モジュールとパッケージ | モジュール境界 — 公開範囲をどこで切るか |
| インターフェイス | 継承や内包より利用 — 「継承を実装しようとしない」の背景 |
| サードパーティーのソース | サプライチェーン / 依存関係 — 何を入れるかの判断 |
| ツール | 静的解析 — go vet をどこで走らせるか |
| build | ビルド — 決定性と冪等性 |
| デフォルトは決まっている | フォーマッター — 書式の議論を機械に預ける |
インストール
※ 下のコマンドの go1.19 は取り込み時点の版。現在の版は配布ページで確認する。
https://go.dev/dl/ or tar -C /usr/local -xzf go1.19.linux-arm64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile
source $HOME/.profile
環境変数を確認
# go env
GOPATH="$HOME/go"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
Goのベストプラクティスに沿う
従来とは違ったGoに合った考え方をする
言語の開発者が意図した通りのコードにする
イディオム的(慣用的な)なコードにする
言語の特徴を最大限生かす
継承を実装しようとしない
まだ十分にテストされていない技術を使って建設された橋を渡る最初の人にはならない
ネット上には間違った情報や古い説明がたくさんある
シンプルで明確にする
明快さと可読性を重視する
※意図を明確にする
※繰り返しが多くなったとしても単刀直入にする
※冗長さは発生する
※例が実践的なコードかどうか
基礎的な概念を理解する
ポインタのメモリレイアウトやガーベジコレクションへの影響など基礎的な概念を理解する
依存関係を最小にする
Goのインストールに他のソフトウェアは必要としない
一連の開発ツールが言語のインストールだけで揃う
異なるバージョンのコンパイラで生成されても同じマシンで実行可能
※コンパイル時にバイナリファイルにGoのランタイムは組み込まれる
※Goのランタイム: メモリの確保, ガーベジコレクション, 並行性のサポート, ネットワーキング, 組み込みの方や関数の実装など
※他の言語だとランタイム環境が別に必要
※バイナリ形式なのでランタイムに依存しない
サードパーティーのソース
サードパーティーのソースは$HOME/go下にダウンロードされる
※go mod tidy
※go install xxx/yyy@latest
※コマンドはgo/bin下
※デフォルトが決まっている
サブリポジトリがある
※These packages are part of the Go Project: https://pkg.go.dev/golang.org/x
デフォルトは決まっている
フォーマットの議論を不要にするためデフォルトを決めている
build
go runは実行ファイルが毎回作成削除されるので、再利用する場合はgo buildを使う
※go mod init済みで、.goファイルがあればgo buildの引数は不要
Makefileを使う
※.DEFAULT_TARGETはbuildにする
※.PHONYはタスクを実行するためのターゲットを指定して同じ名前のディレクトリがあるときの混乱を防ぐ
※makeで実行
※make fmt等で個別に実行できる
※タブでインデントする
ツール
go fmt
go imports(golang.org/x/tools/cmd/goimports)
※importsはアルファベット順に、不要なものを削除、未指定で使用しているものを追加
※goimports -l -w .
※.以下のすべてのサブディレクトリも対象
staticcheck
※変数の命名方法、エラーメッセージのフォーマット、パブリックなメソッドや型等の指摘
※修正不要なケースもあるが、修正の検討をする
※honnef.co/go/tools/cmd/staticcheck
※go.modファイルがあることが実行の前提条件
※レビュープロセスに組み込む
go vet
※期待通りに動いてくれないもの、引数の数が合っていない、使われない変数への値の代入等を指摘してくれる
※ビルドプロセスに組み込む
golangci-lint run
※github.com/golangci/golangci-lint/cmd/golangci-lint
※staticcheck, go vetをまとめてくれる
※設定は.golangci.ymlで調整
IDE
Visual Studio CodeとGoLandが人気
※VSCodeはGo for Visual Studio Codeをインストールする
※GoLandはJetBrainsのIDEで有料
※IDEよりもMakefileの方がビルドプロセスを誰が、どこで、いつ実行しても繰り返し自動的に実行できる
バージョンアップ
半年毎にメジャーアップデート
マイナーアップデートはバグやセキュリティ関連の問題が見つかるたびにリリース
最新のものを使う
※最新のものをインストールすれば古いバージョンは自動的に削除される
※Go自体のインストールとgo install共に
整数リテラル
3桁ごとに_
10進数を使う
runeリテラル
シングルクォーテーションで囲う
※'\n', '\t', '\'', '\"', '\'
文字列リテラル
runeリテラルを含むことができる
\や改行や"を含めたい場合はエスケープするか``で囲う
ゼロ値は""
イミュータブル(変更不可能)
※別の値の代入はできるが、変数に代入された文字列の値を変えることはできない
スライス式を使用可能だが使わない
※スライスのようにメモリを共有したり、他のスライスに変更が影響したりしない
※スライス式は1バイトの文字のみが使われている前提
※stringsやunicode/utf8を使う
byteはstringに型変換可能
※水面下でバイト列を使っている
リテラルと型
リテラルには型はない
明示的に指定されるまで型は強制されない
※リテラルと互換性がある任意の変数に代入できる
※互換性がない任意の変数に代入するとコンパイル時にエラーになる
※文字列リテラルを数値型の変数に代入はできない
※数値リテラルを文字列の変数に代入できない
※浮動小数点型のリテラルを整数に代入できない
整数型
byte(uint8: 0~255のエイリアス)
※uint8は使わない
int(int64)
※64ビットのCPUの場合
※範囲を超えることはまずない
※整数のデフォルトリテラル
ゼロ値は0
※浮動小数点型も
浮動小数点
float64
※浮動小数点は10進数を正確に表現できない
※誤差が許容される場合に使う
※金額など誤差なしで10進数を表現しなければならない場合には使わない
※グラフィックスや物理計算のときだけ使う
※==や!=で比較しない、範囲の判定にする
rune型
コードポイントを表現する型
int32のエイリアス
型変換
暗黙的な型変換は行わない
型が違う場合は型変換を明示的に行う
論理型
数値や文字列をtrueとしては扱えない
変数
右辺の値で変数の型は省略できる
※リテラルのデフォルトの型になる
関数の外では変数の宣言はしない
関数内では:=を使う
※関数の外のvarはパッケージレベルの変数になる
※パッケージレベルの変数の値の変更は追跡が困難でデータフローの理解がしずらくなる
※パッケージレベルの未使用変数はチェックされない
※パッケージレベルはイミュータブルな変数のみ宣言する
※イミュータブルな変数以外でパッケージレベルでの変数の宣言は使わない
:=以外の変数宣言のケース
※x := 0をvar x intとして明示的にゼロ値で初期化
※x := byte(20)をvar x byte 20として、リテラルのデフォルトの型と変数の型が異なる場合
スネークケースではなく、キャメルケースを使う
※基本的に使わない
関数内では短い名前を使う
変数のスコープ(有効範囲)が狭ければ狭いほど短い名前を使う
※1文字の変数も珍しくない
※for-rangeのキーと値はkとvを使う
※forはiやjがループカウンター
※intはi, floatはf, boolはbを使うのが慣例
※短い変数名が使いづらい場合はそのブロックの処理が複雑すぎるので、処理をリファクタリングすべき
型を変数名には含めない
※型は明確なため
※型Tが基本型(論理型, 数値型, 文字列型)あるいは型リテラルの場合、Tの基底型はT
※それ以外の場合、Tの型の宣言で参照している型がTの基底型になる
定数
宣言はパッケージレベルもしくは関数内
const (
xxx = 10
yyy = 20
)
型のない定数はリテラルに名前を付けているだけ(コンパイル時に決定できる値を保持しているだけ)
※デフォルトの型はある
変数がイミュータブルであることを宣言する方法はない
※Goが値渡しのため、定数がイミュータブルでも問題にはならない(書き換えられることはない)
※型のない定数に数値リテラル, true/false, 文字列, rune(コードポイントを表現する型でint32のエイリアス),は代入できてしまう
※型のない定数にcomplex, real, imag, len, capの結果も代入できてしまう
※型のない定数に上記値と演算子の式を代入できてしまう
型のある定数には同じ型の変数に対してのみ代入できる
※複数の数値型と比較等をする可能性がある場合は型なしにする
未使用の場合はチェックされず、バイナリファイルに含まれない
イミュータブル(変更不可能)
Goには実行時に計算された値がイミュータブルであることを指定する方法はない
イミュータブルな配列、スライス、マップ、構造体はない
構造体のフィールドがイミュータブルであることを宣言する方法はない
※関数の中で変数の値が変更されているかどうかは明白なため
未使用変数
コンパイル時にチェックされるが、ただ代入されるだけの実質的に使われていないもののチェックは行われない
※golangci-lintは指摘してくれる
パッケージレベルの未使用変数はチェックされない
配列(固定長)
var x = [3]int{10, 20, 30}
var x = [...]int{10, 20, 30}
※...は大きさを表す
※[n]あるいは[...]と書くと配列になる
使われることはめったにない
※事前にサイズがわかる場合のみ使う
全要素が常に同じ型とは限らない
要素を超えるインデックスや負のインデックスを指定するとコンパイルエラーになる
配列の大きさに変数を使うとコンパイルエラーになる
※配列の大きさはコンパイル時に決定できないといけない
配列の大きさが異なれば別の型になる
※大きさが異なる配列への型変換や代入はできない
※任意の長さの配列を扱う関数は書けない
よく使われるスライスの後方支援のために存在している
スライス
var x = []{10, 20, 30}
※[]と書くとスライスになる
var x [][]int
可変長の配列
スライスの型にはサイズは含まれない
※ゼロ値はnil
※var x []intのxはnil
要素を超えるインデックスや負のインデックスを指定するとコンパイルエラーになる
スライス同士は比較できない
※比較できるのはnilとだけ
※reflect.DeepEqualで比較できるが、テスト用の関数
サイズはlenで確認できる
要素追加はappend
var x []int
x = append(x, 10)
※1つ
x = append(x, 10, 20, 30)
※複数
y := []int{20, 30, 40}
x = append(x, y...)
※別のスライスの全要素
xのコピーが渡され、xのコピーに値が追加されたものが返される
スライスのために確保されたメモリの容量(キャパシティ)を確認する場合はcap
※lenの値がキャパシティに達すると新たに領域を確保される
※例: 1024未満の場合は2倍、それ以降は25%増やす
キャパシティの指定はmakeを使う
※実行すればスライスのサイズが予想できるが、書いている最中には分からない場合はmakeを使う
x := make([]int, 0, 10)
※lenは0, capは10
x := append(x, 1)
スライスが大きくならない可能性がある場合
var data []int
※nil
x := []int{}
※nilではなく、長さ0の空のスライスリテラル
スライスのスライスは同じメモリ領域を共有し、一方の変更が他方に影響するので変更はしない
※appendする際にはフルスライス式(x[low:high:max])を使う
※maxはサブスライスの容量のmax
※サブスライスの長さとキャパシティを同じにする
サブスライスにメモリを共有しない場合はcopyを使う
x := []int{1, 2, 3, 4}
y := make([]int, 2)
copy(y, x[1:])
※yは[2, 3]になる
合成型
複合データ型、コンテナ型とも呼ばれる
nil
型はないため、異なる型に代入、比較したりできる
値がないことを意味する
マップ
構造体を使う
※マップはAPIを定義しない
※マップは特定のキーだけを含むような定義ができない
※マップはキーの値はすべて同じでなければならない
ゼロ値はnil
※{}なしで宣言されることはないので、ゼロ値nilのマップを使うことはない
※値を書き込めない
あらかじめ要素の個数が分かっている場合はmakeを使う
ages := make(map[string]int, 10)
マップ同士の比較はできない
※nilとの比較はできる
Goではハッシュマップを使っている
※内部的には配列を使用
※キーはハッシュアルゴリズムで数値に対応付けられ、配列のインデックスとして使われる
※配列の各要素はバケットと呼ばれる
※各バケットも配列で2つのキーが同じバケットに対応付けることができる
※キーの衝突が多いとバケット内のすべてのキーについて確認するため、データの取り出しに時間がかかる
※ある程度の数の要素が追加されると、サイズを大きくしてバケットを調整して、多くの要素を記憶しても遅くならないようにしている
データ取得はv, ok := m[key]
※値がない場合は値の型のゼロ値が返される
※2つ目の戻り値にキーが存在するかのtrue/falseが返ってくる
※ゼロ値かintに0が設定されているかはv, okで区別することができる
データ削除
delete(m, key)
※マップがnil, キーがない場合は何も起こらない
セット
要素の値に重複がなく、要素間に順序がないデータ型
マップか構造体を使う
マップ
※マップがキーが重複しないことを利用する
intSet := map[int]bool{}
vals := []int{5, 10, 2}
for _, v := range vals {
intSet[v] = true
}
if intSet[5] {
// 含まれている
}
構造体
intSet := map[int]struct{}
vals := []int{5, 10, 2}
for _, v := range vals {
intSet[v] = struct{}{}
}
if _, ok := intSet[5]; ok {
// 含まれている
}
構造体
type 型名 struct {
name string
age int
pet string
}
var bob personとbob := personは同じ
初期化はフィールド名と値を指定する
※値だけの場合はすべてのフィールドを指定しないとエラーになる
※フィールド名と値の場合はすべてのフィールドを指定する必要はない
無名構造体
※外部データを構造体に変換したり、構造体を外部データに変換するときに使う
※テーブル駆動のテストを書くときにも使う
pet := struct {
name string
kind string
} {
name: "ポチ",
kind: "dog",
}
型が同じまたは、どちらかが無名構造体でフィールドにスライスやマップや関数やチャネルがない場合のみ構造体同士を比較することができる
※フィールドの名前, 順番, 型がすべて同じでも構造体の型が別であれば比較できない
型変換はフィールドの名前, 順番, 型がすべて同じときだけ可能
シャドーイング
内側のブロックで同じ名前の変数があると、外側のブロックの変数にはアクセスできなくなる
※パッケージ名も同様
※ユニバースブロックの識別子も同様: https://go.dev/ref/spec#Predeclared_identifiers
Types:
any bool byte comparable
complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr
Constants:
true false iota
Zero value:
nil
Functions:
append cap close complex copy delete imag len
make new panic print println real recover
シャドーイングはshadowリンターで検知する
※go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
※パッケージ名とユニバースブロックの識別子シャドーイングは検知されない
if
条件は()で囲わない
※コンパイラが()は関数呼び出しで末尾に;を挿入するため
ifの条件内で定義した変数はelse if内やelseのブロックでも使える
for
for i := 0; i < 10; i++ {
}
i := 1
for i < 100 {
i = i * 2
}
※whileの代わり
for {
}
※無限ループ
forから抜ける場合はbreak, 以降の処理をせず、次のループ処理を実行するにはcontinue
※2つ以上も可能
outer:
for _, target := range targets{
for i, v := target {
if v < 10 {
continue outer
}
}
}
※continue: outer
for i, v := range target {
}
for _, v := range target {
}
for k := range target {
}
※組み込みの合成型(複合データ型)とそれらをベースにしたユーザー定義の型で使える
※セキュリティ(HashDoS: すべてのキーが同じバケットにハッシュされるようにデータが送信される)を考慮して、各要素の順番は毎回異なる
※i, v, kはコピー
switch
switch size := len(arr); size {
case 1, 2, 3, 4
//
default:
//
}
※sizeはすべての分岐内で使える
※break(次のcase文の実行を防ぐ)は不要
switch wordLen := len(word) {
case wordLen < 5
//
case wordLen > 20
//
default:
//
}
※比較が使える
※複数条件の場合はif/elseではなく、これを使う
関数
func (*s structName) funcName(arg1 int, arg2 string) (int error) {
if ... {
return nil, error
}
...
return i, nil
}
※戻り値の変数を使わずにreturnした場合はreturnした値が優先されるので、戻り値に名前は付けない
※空returnは明示的ではないので、戻り値に名前は付けない
戻り値に名前を付ける場合はdeferで実行された関数が外側の関数の戻り値を検証したり、変更したりするとき
func DoSomeInserts(......) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err == nil {
tx.Commit()
}
if err != nil {
tx.Rollback()
}
}()
// SQLを実行していく
.....
.....
return nil
}
名前付き引数やオプション引数はないので、代わりに構造体を関数に渡す
可変用引数
※vals ...int
※...intでintのスライスになる
※変数を可変長引数に渡すときはv...として渡す
関数は値
※値として変数に代入して、引数に渡せる???
関数型
type xxxFunc func(int, int) int
※いつ使う???
無名関数
func(j int) {
}(i)
※deferとgoroutineの起動で使う???
クロージャーはいつ使うか
※パッケージレベルの関数定義を減らして、名前の衝突を防ぐ
※関数引数を使って、関数内の変数を関数の外で使えるようにする
sort.Slice(people, func (i int, j int) bool {
return people[i].Age < people[j].Age
})
※値渡しのはずが、people(スライス)を変更することができる
defer
ファイルやネットワークで使用した一時的なリソースのクリーンアップに使う
関数内で複数のクロージャーをdeferできる
LIFO(last-in-first-out)
return文の後で実行される
名前付き戻り値(err等)を使って、関数の戻り値を検証したり変更したりできる
※err次第でコミット or ロールバック等をする
tryではなく、deferの理由はネストを浅くするため
https://link.springer.com/article/10.1007/s10664-017-9508-2
>提案された11のコード特性のうち、複雑さの増大に著しく影響するのは2つだけです。入れ子の深さと構造の欠如です。
※11のコード特性: https://link.springer.com/article/10.1007/s10664-017-9508-2/tables/2
値渡し
Goは関数の引数には必ず引数の値のコピーを渡す
※引数の値は変えられない
※構造体のフィールドも変えられない
※マップやスライス、構造体のフィールドのマップやスライスはポインタを使っているため変更できる
ポインタ
ポインタも変数
※別の変数が保存されているメモリのアドレス
※ポインタはどのような型を参照していてもサイズが同じ
ポインタのゼロ値はnil
※スライス, マップ, 関数, インターフェイス, チャネルはポインタとして実装されている
pointerToX := &x
※型はポインタ型 or ポインタ
pointerVal := *pointerToX
※nilの場合はパニックになる
構造体は&を付けてポインタのインスタンスを作る
基本型のリテラル(数, ブール値, 文字列)や定数はコンパイル時のみ存在し、メモリ内のアドレスをもたないため&を付けることができない
※構造体のフィールドに基本型へのポインタ(*string等)がある場合はヘルパー関数を使う
※stringp("Xxx")
func stringp(s string) *string {
return &s
}
インスタンスや関数がメソッドに渡される際にコピーされるの値はインスタンスへのポインタ
Goは基本型・構造体にポインタで渡すか値で渡すかを「選択させてくれる」
値で渡すべき
※データの変更が追いやすい
※ガーベジコレクタの仕事量が減る
Goでイミュータブルを宣言できないことは、引数で値で渡すかポインタで渡すかを選択できるため問題とはならない
※Goではポインタを使って引数がミュータブルであることを示す
※基本型, 構造体, 配列といった非ポインタ型は元のデータを変更できない(イミュータブルを保証)
※関数にポインタのコピーが渡されても、同じデータを参照するため、元データの変更が可能となる
※ポインタとしてnilを渡すと値を変更できない
ポインタ引数を使わなければならないのは関数がインターフェイスを受け取るときだけ
※JSONを扱うとき
※JSONで値なしに設定できる構造体フィールドにはポインタ型を使う(ゼロ値と値なしを区別する)
ポインタ型を返すのはデータ型の中に変更するべき状態があるときだけ
※入出力でI/Oを扱う際のバッファを使ったデータの読み書き
並行実行に使用されるデータ型は必ずポインタとして渡す
関数に10メガバイトのデータを渡すのには1ミリ秒かかる
関数から100バイトのデータを返すのには10ナノ秒、同じデータを参照するポインタを返すのには30ナノ秒かかる
※関数から返す際にポインタの方が速くなる閾値は1メガバイト
※10メガバイトのデータを返すのに2ミリ秒かかり、同じデータへのポインタを返すのには0.5ミリ秒しかかからない
ほとんどの場合、値を使うかポインタを使うかはパフォーマンスに影響を与えない
※関数がやり取りするデータの値かポインタかの閾値はメガバイトを超えるかどうか
※計測: https://github.com/learning-go-book/pointer_performance
引数としてnilを渡したり、引数として渡した構造体のフィールドにnilがあったりした場合、関数内ではそこに値を設定できない
※nilはアドレスではないため
ポインタを「値なし」を示すのに使うのではなく、値型とブール値を返す
※関数からnilに設定したポインタを返すのではなく、値とブール値を返す
※値を変更しない場合
マップは構造体へのポインタとして実装されている
※関数に渡されたマップの変更は元のマップに反映される
※関数にマップを渡さすとマップのポインタがコピーされる
※マップはパラメーターや値を返すのには使わない
※動的言語に慣れている場合は使いがち
※マップは内容が明示的ではない
※マップではなく、構造体を使う
※ガーベジコレクタの負荷軽減のためにもマップではなく、構造体を使う
スライスはサイズ、キャパシティ、メモリブロックを参照するポインタとして実装されている
※関数に渡されたスライスの内容の変更は元のスライスに反映される
※サイズとキャパシティの変更はコピーに対して行われるだけで、元のスライスには反映されない
※appendで元のスライスは変更されない
※関数内でスライスの内容は変更しない
スライスはファイルや外部ネットワーク等の外部リソースからデータを読み込む場合のバッファとして使う
※多くの言語はファイルや外部ネットワーク等の外部リソースからデータを読み込む場合に大量に不必要なメモリ割り当てが生じる
※Goではスライスを一度だけ作成し、データを読み込むバッファとして使う
※ガーベジコレクタの負荷軽減のため
ガーベジとはどのポインタにも参照されていないデータ
※そのデータを参照しているポインタがなくなれば、該当のメモリは解放され、再利用される
※メモリの解放ができないと、メモリ使用量は増え続け、RAMを使い切ってしまう
ガーベジコレクタはどのポインタにも参照されていないデータのメモリを自動的に見つけ、解放する
※ガーベジコレクタがあるからといって、ガーベジをたくさん作らない
スタックは一連のメモリブロック
※実行されるスレッドのすべての関数呼び出しが同じスタックを共有する
※スタック上のメモリ割り当ては高速で単純
※メモリの最後に割り当てた位置は「スタックポインタ」に保存される
※追加でメモリを割り当てる場合にはスタックポインタを動かす
※関数呼び出し時はスタックフレームが生成される
※ローカル変数は引数と共にスタックフレーム内に保存される
※変数定義の度にスタックポインタはサイズ分だけ動かされる
※戻り値はスタックフレーム経由
※呼び出し完了後にはスタックポインタがスタックフレームの最初の位置まで戻され、メモリが解放される
※各goroutineが自分自身のスタックを持っている
※goroutineの管理はGoのランタイムが行っているため、プログラム実行中にスタックのサイズを増やすことが可能
※スタックを拡張する際にはスタックのデータ全部をコピーする必要があり、時間がかかる
※スタックが拡張と収縮を繰り返すのは最悪
※コンパイル時にデータの大きさが正確に分かっていればスタックに保存できる
ポインタが参照するデータをスタックに保存する条件
※コンパイル時にデータのサイズが分かっているローカル変数であること
※そのポインタを関数から返していないこと
※ポインタ変数を返すと、関数が終了するとそのポインタが参照するメモリは解放されてしまう
※コンパイラがデータをスタックに保存できないと判断すること: エスケープ
ヒープはガベージコレクタが管理するメモリ
※スタックのようにスタックポインタを動かすだけよりも複雑
※ガベージの発見が複雑
※ヒープのデータは参照しているポインタ型変数(or データが参照しているポインタが入ったデータ)がなくなればガベージになる
※ローカル変数へのポインタが返されると、ローカル変数はヒープに保存される
Goコンパイラが行うエスケープ解析は完全ではない
※ヒープに保存されるべきデータをスタックに置くこと(メモリ破壊が起きる)がないようになっている
※スタックに保存できるはずのデータがヒープにエスケープしてしまう場合もある
※Goのバージョンが上がる度にエスケープ処理は向上している
Goのランタイムのガベージコレクタは遅延時間の減少を優先している
※反応速度を短く保つべき: https://research.google/people/jeff/, https://research.google/pubs/pub40801/
※1回のスキャンでほとんどのガベージを見つけるよりも、スキャンをできるだけ短時間で行う
※1回のスキャンを500マイクロ秒より短くするようにデザインされている
※The Journey of Go's Garbage Collector: https://go.dev/blog/ismmkeynote
RAMからの読み出しでシーケンシャル(逐次的)や読み出しが一番高速
※構造体や基本型のスライスはすべてのデータがメモリ内にシーケンシャルに展開される
※構造体へのポインタ(あるいはポインタをフィールドにもつ構造体)のスライスは、データがRAM上に散らばっており、低速
※Memory Bandwidth Napkin Math: https://www.forrestthewoods.com/blog/memory-bandwidth-napkin-math/
※RAM内にばらばらに保存されたデータをポインタ経由でアクセスするとほぼ2桁遅くなる
※mechanical sympathy(今何をしているか理解して、性能を最大限引き出す)
※Goのベストプラクティス(Goのイディオム)に従っていれば、自動的に性能を最大限に引き出せる
※Javaではオブジェクトはポインタとして実装されていて、インスタンス変数はポインタがスタックでデータはヒープに保存されている
※Javaはメモリ内のデータを読み出すのにあちこち飛び回る必要がある
※Java仮想マシンには賢いガベージコレクションが複数入っていて、スループットの最適化や遅延を短くするように最適化されている
※Python,Ruby,Javascriptの仮想マシンはそこまで最適化されていない
※Language Mechanics On Stacks And Pointers: https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html
※Allocation efficiency in high-performance Go services: https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
型
抽象型は何をするかを定義するだけ
※実装を提供しない
具象型は何をどのようにするかを規定する
※データの記憶のされ方を規定
※型に付随して定義されるメソッドの実装を提供する
すべての型は基底型を持っている
※基本型(論理型、数値型、文字列型)と型リテラル(type literal)の場合、Tの基底型はT
※それ以外はTの宣言で参照している型がTの基底型
型
※コンパイルの段階で間違った操作を発見できる
※間違った操作を行えないようにする
※特定の型に対して共通の操作(関数など)を記述して、その型を統一的に扱う
※ドキュメント
※コードが表す概念に名前を付けることができる
※intよりもPercentage型
※同じデータをベースにしていても、別の作業をする場合には型を2つ作る
※2つの型に関連があることが明確にすることができる
type Person struct {
LastName string
FirstName string
Age int
}
※基底型は構造体リテラル
type Score int //基底型はint
type Converter func(string)Score // Scoreを戻り値とする関数型
任意のレベルのブロックで型を宣言できる
型へのアクセスは定義したスコープ内
※パッケージブロックレベルの型がエクスポートされた場合は例外
ユーザー定義型を元に新しいユーザー定義型を作成できる
type Score int
type HighScore Score
type Employee Person
※型の間に継承関係はない
※int, Score, HighScoreは型変換をしないと代入できない
※ScoreのメソッドはHighScoreでは定義されない
※ScoreもHighScoreもint(基底型が基本型)の演算子は使える
※同じ型を基底型にしているだけ
iotaは使わない
※明示的に定義する
クラス継承よりもオブジェクト合成
※コードの再利用
※Goには継承はないが、合成や昇格はある
type Employee struct {
Name string
ID string
}
type Manager struct {
Employee // 型名のみでNameとIDが加わる(合成)
Employees []Employee // 部下
}
※EmployeeのメソッドはManagerのメソッドとして呼べる(昇格)
※上位の構造体に埋め込みフィールドと同名のフィールドやメソッドがある場合は、埋め込まれているほうが隠される
※埋め込まれているフィールドの型を明示的に指定する
※m.X or m.Employee.X(EmployeeとManagerにフィールドやメソッド名が同じXがある場合)
※Manager型の変数をEmployee型の変数に代入することはできない
※埋め込まれたフィールド側からは、自分が埋め込まれていることを知る方法はない
※埋め込んだ構造体から、埋め込まれている構造体と同名のメソッドを呼んだ場合は埋め込まれている構造体のメソッドが呼ばれる
※埋め込まれたフィールドのメソッドは上位の構造体のメソッドセットに含まれるだけ
メソッド
type Person struct {
LastName string
FirstName string
Age int
}
func (p Person) String() string {
return fmt.Printf("%s %s: 年齢%d歳", p.LastName, p.FirstName, p.Age)
}
※レシーバにp Personを指定
※レシーバ名は型名の先頭1文字を使う
※Person専用のメソッド
※型の宣言と同じファイルで定義する
p := Person {
LastName: "田中",
FirstName: "健太",
Age: 32,
}
output := p.String()
fmt.Println(output)
ポインタレシーバ
※(c *Counter)
※cはポインタ
※c.Xxx()は内部で自動的に(&c).Xxx()に変換される
※レシーバを変更する場合
※メソッドがnilを扱う場合
※型にポインタレシーバのメソッドがひとつでもある場合
※ポインタインスタンスのメソッドセットに含まれる
※値インスタンスのメソッドセットに含まれない
※関数の引数にポインタを渡すときと同様にポインタのコピーが渡される
※コピーのポインタを変更しても、オリジナルは変更されない
※nilを受けて、nilではないものに変更できない
値レシーバ
※cはコピー
※(c Counter)
※メソッドがレシーバを変更しない場合
※メソッドがnilを扱わない場合
※型にポインタレシーバのメソッドがひとつもない場合
※ポインタインスタンスのメソッドセットに含まれる
※値インスタンスのメソッドセットに含まれる
メソッドと関数
※共に変数に代入して、呼び出したり、他のメソッドや関数に引数として渡すことができる
※起動時に設定された値や実行中に変更された値は構造体に保存し、ロジックはメソッドに実装
※ロジックが入力引数のみに依存するなら関数
メソッド
関数
※型自体から関数を作成(メソッド式)でき、Xxx(receiverInstance, arg)のように呼べる: func(Adder, int) int
インターフェイス
type Stringer interface {
String() string
}
--------------------------------------------------
type LogicProvider struct {}
type Logic interface {
Process(data string) string
}
type Client struct {
L Logic
}
func (c Client) Program() {
...
c.L.Process(data)
}
func main() {
c := Client{
L: LogicProvider{},
}
c.Program()
}
--------------------------------------------------
func process(r io.Reader) error
r, err := os.Open(fileName)
if err != nil {
return err
}
defer r.Close()
// デコレート
// gz, err := gzip.NewReader(r)
// if err := nil {
// return err
// }
// defer gz.Close()
// gzipの場合はreturn process(gz)
return process(r)
--------------------------------------------------
type ReadCloser interface {
Reader
Closer
}
--------------------------------------------------
※~er(~するもの)と命名する
※標準ライブラリにすでに要求を満たすインターフェイスがあれば、それを使う
※インターフェイスのメソッドセットを完全に含むかどうかだけ
※インターフェイスによってコードの依存関係(何を渡せば呼び出せるのか、どのような機能が要求されるのか)が明確になる
※インターフェイス型のフィールドに具象型として渡される場合に実装すべきメソッドが明確になる
※GoF: クラス継承よりオブジェクト合成のほうがよい
※GoF: 実装ではなく、インターフェイスに従ってプログラムせよ
※具象型では、どのインターフェイスを実装するかを明示しない(implementsは不要)
※具象型は新しくメソッドを自由に追加できる
※ファイルの読み込みだけなら、そのファイルのインスタンスを参照するのにインターフェイスio.Readerを使うことで他のメソッド(io.Writerのメソッド)は無視できる
※暗黙的な実装のインターフェイスによって、必要に応じて実装を交換することができ、変化に対してコードが進化できるようになる
※任意の型の値を代入できる変数を定義できる
※インターフェイス型の変数は、そのインターフェイスが特定する型の集合に属する任意の型の値を記憶できる
※インターフェイス型を持つ変数やフィールドに具象型を代入できる
標準インターフェイスを使って、デコレータパターンを使う
※インターフェイスのインスタンスを受け取って、そのインターフェイスを実装する別の型を返すファクトリ関数を書く
※例: os.Openが返すos.Fileのインスタンスはインターフェイスio.Readerを満たせば、どのコードに使える
※gzip.NewReaderはio.Readerを満たす
※例: io.Readerをもうひとつ別のio.Readerでラップする(ラップするコードを追加するだけで非圧縮・圧縮ファイルの読み込み処理が書ける)
インターフェイスを受け取り構造体を返す
※関数ではインターフェイスを受け取り、ビジネスロジックを実行し、構造体(具体的な型)を返す
※インターフェイスを受け取ることでどのような機能が使われているかが明確になる
※インターフェイスを受け取ることで具体的なインスタンスに依存しないため、将来の柔軟性が高まる
※具体的な型はメソッドやフィールドを追加しやすい
※インターフェイスを返し、新しいメソッドを追加するとインターフェイスのすべての具象型で実装が必要になる
※さまざまな種類のトークンを返すパーサーなどインターフェイスを返す以外に選択の余地がないこともある
※errorインターフェイスは例外(異なる実装が返される可能性が高いため、インターフェイスですべての可能性を処理する必要がある)
※ファクトリ関数を1つ作って引数で異なるインスタンスを返すより、具体的な各型に別々のファクトリ関数を作成する
※関数をインターフェイス型の引数で呼び出すと各インターフェイス引数がヒープに割り当てられてしまうので、パフォーマンスは犠牲になる
※遅くなって、パフォーマンスを重視する場合は具象型の引数にする
インターフェイスはベースとなる型へのポインタとベースとなる値へのポインタの組で実装されている
※インターフェイスのインスタンスのゼロ値はnil
※型と値がnilのとき、インターフェイスはnil
※nilの具象型インスタンスを代入されたインターフェイス変数に対してメソッドは呼べる
※nilのインターフェイスに対してメソッドは呼べない
※nilではないインターフェイスに対してはメソッドを呼べる
※非nil型のインターフェイスのインスタンス(== nilはfalse)の値がnilかどうかはリフレクションを使う必要がある
--------------------------------------------------
any
※var i any(var i interface{}と同じ)
※外部ソース等で形式が不明なデータ、または型を制限したくない場合にanyが使える
---
data := map[string]any{}
...
json.Unmarshal(contents, &data)
---
インターフェイスの具象型のチェック
---
hoge, ok := i.(Hoge)
---
※特定の型かチェック
※iはインターフェイス型
※hogeはHoge型
※この型アサーションはインターフェイス型にのみ適用可能で実行時にチェックされ、型が変わる
※型変換は具象型にもインターフェイスにも適用され、コンパイル時にチェックされ、型が変わる
※基底型がわからない場合はリフレクションを使う
※型によって処理を分ける場合
---
switch j = i.(type) {
case nil:
case Hoge:
default:
}
---
インターフェイスが別のインターフェイスを実装しているか調べる
---
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src);
}
...
}
---
※型アサーションを使うのは無駄な処理をしないために早めreturnする場合
インターフェイスが別のインターフェイスを実装しているか調べる
---
func ctxDriverStmtExec(ctx context.Context, si driver.Stmt,
nvdargs []driver.NamedValue) (driver.Result, error) {
if siCtx, is := si.(driver.StmtExecContext): is {
return siCtx.ExecContext(ctx, nvdargs)
}
// フォールバックの処理
}
---
※StmtExecContextはExecContextメソッド(Execメソッドをコンテキスト対応したメソッド)を定義
※後からコンテキスト(キャンセルに使用する)用のメソッドが追加されたケース
ラップされた実装の中でオプションのインターフェイスが実装されていた場合には検知できない
※型アサーションの欠点
※デコレータパターンが該当
※ラップされたエラーの有無やアクセスにはerrors.Isやerrors.Asを使う
インターフェイスに想定されていなかった実装を防ぐにはインターフェイスをエクスポートせず、メソッドをひとつエクスポートしない
※別のパッケージの構造体に埋め込めなくなる
関数型にもメソッドを定義できる
---
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
---
※func(http.ResponseWriter, *http.Request)のシグネチャであれば、http.HandleFuncに型変換し、http.Handlerとして使える
Goは小さなインターフェイスを推奨
※メソッドがひとつしかないインターフェイスは関数型の引数を簡単に置き換えられる
インターフェイス引数 or 関数型引数
※他の多くの関数や、引数に指定されていない他の状態変数に依存する場合はインターフェイスを引数にし、関数をそのインターフェイスにつなぐための関数型を定義する(httpパッケージのHandler等)
※sort.Slice等、単純なものであれば、関数型の引数にする
依存性の注入は暗黙のインターフェイスを使う
---
func main() {
l := LoggerAdapter(LogOutput)
ds := NewSimpleDataStore()
// ロジックの引数を変えれば、ログやデータ保存の仕組みを簡単に変えることができる
logic := NewSimpleLogic(l, ds)
c := NewController(l, logic)
// c.SayHelloは関数
// http.HandlerFuncは関数を引数に取り、それを関数型http.HandleFuncに変換している
http.HandleFunc("/hello", c.SayHello)
http.ListenAndServe(":8080", nil)
}
---
※依存性の注入(Dependency Injection: DI)は必要な機能を明示的にする
※The Dependency Inversion Principle: https://web.archive.org/web/20110714224327/http://www.objectmentor.com/resources/articles/dip.pdf
※Bad Design:
rigidity: hard to change(change affects too many other parts)
fragility: change and unexpected system break
immobility: hard to reuse in another app
※他言語だと大きく複雑なフレームワークを使うが、Goは暗黙のインターフェイスを使うだけ
※暗黙であることでフィールドのインターフェイスの具象型は変化させやすくなる(具象型に依存しない)
※依存性の注入でテストも簡単
※wireを使うとmain内に作成した具象型の宣言を自動的に作成してくれる: https://github.com/google/wire
※具象型が何であるかはmainだけで書く
※ファクトリ関数で個々の具象型を作成する
※ファクトリ関数の引数に具象型のインスタンスを渡すことで依存性を注入する
※ファクトリ関数の引数の具象型のインスタンスを変えることで変化に対応する
※Goにはメソッドのオーバーライドも継承もない、オブジェクトもないに等しい
※Goには関数型もクロージャもあるが、関数型言語とは言えない
※Goをオブジェクト指向型言語や関数型言語のように使おうするとイディオムに反するコードになる
※Goは実用的で、シンプルで読みやすく、保守しやすい
エラー処理
errorかnilを返す
※呼び出し側ではnilチェックをし、エラー処理か独自エラーを返す
※エラーメッセージは小文字始まりで、最後に.や改行は置かない
※エラー以外の他の値はゼロ値を返す
※イディオム的Goでは多少長くなっても、明解なコードが好まれる
※エラーがないときはnilを返す
※インターフェイスがnilかどうかは基底型と値が共にnilであるときだけ
※独自のエラー型だけを返さない
※独自エラーでも戻す型はerrorにすることで呼び出し側が特定のエラーに依存しない
type error interface {
Error() string
}
※fmt.Printlnにerrorを渡すとErrorメソッドが呼び出される
固定の文字列の場合はerror.New("xxxxx")
※エラー文字列はErrorメソッドを呼び出すと返される
センチネル(番兵、番人)エラーパッケージレベルで宣言される読み込み専用の変数
---
if err == zip.ErrFormat {
fmt.Println("ZIP形式ではありません")
}
---
※エラーをチェックするだけでなく、適切に処理する: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
※例: zip.ErrFormat, rsa.ErrMessageTooLong, context.Canceled
※定義するとパブリックになるので、他から依存されてしまう
※標準ライブラリのものか、エラー型を定義して使うべき
※センチネルエラーはラップすると==でチェックできず、型アサーションや型switchは使わず、errorのIsかAsを使う
独自エラー(ステータスあり、他のエラーをラップする)
---
type StatusError struct {
Status int
Message string
Err error
}
func (se StatusErr) Error() string {
return se.Message
}
func (se StatusError) Unwrap() error {
returnse.Err
}
---
エラーに付加的なコンテキストを加える(ラップする)
※fmt.Errorf("in fileChecker: %w", err)
※他のエラーを%wを使ってラップするのが慣習
※%wは他のエラーの文字列を含み、オリジナルのエラーの文字列を追加する場合に使う
※カスタマイズしたエラー型でエラーをラップしたい場合はUnwrapを実装しなければならない
特に独自のエラーでラップせず、他のエラーの文字列から新しいエラーを生成したい場合
---
fmt.Errorf("internal failure: %v", err)
---
特定のインスタンスや値を探しているときはerrors.Isを使う
---
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file doesn't exist")
}
}
---
※デフォルトではerrors.Isは==を使って比較
※独自エラーが比較可能ではない場合はIsを実装する
---
func (xe XxxErr) Is(target error) bool {
if xe2, ok := target.(XxxErr); ok {
return reflect.DeepEqual(xe, xe2)
}
return false
}
---
errors.As
※エラーのチェーン内で特定の型にマッチするエラーがあるかチェックできる
※errors.As(調査中のエラー, 探しているエラーの型の変数のポインタ)
※Asメソッドの実装はリフレクションが必要になるが、一般的ではないときだけ実装
---
err := XxxError()
var yErr YyyErr
if errors.As(err, &yErr) {
return fmt.Println(yErr.Code)
}
---
パニック時
※その関数のdeferを実行して、呼び出し側のdeferを実行していく
※recoverを使って、ログを書き出し、os.Exit(1)で終了するのが最も安全
APIの境界を超えてパニックを伝播させず、エラーに変えたい場合
defer func() {
if v := recover(); v != nil {
fmt.Println(v)
return XxxErr
}
}()
デフォルトではGoはスタックトレースを出力しない
※出力したい場合はfmt.Printfで%+v(verbose output)を指定する
※https://pkg.go.dev/github.com/pkg/errors
モジュールとパッケージ
大きい方からリポジトリ、モジュール、パッケージの概念を使って管理する
※モジュールはGoのライブラリあるいはアプリケーションのルート
※モジュールは1個または複数のパッケージから構成される
リポジトリに複数モジュールを保存することは推奨されない
※1リポジトリ1プロジェクト(1モジュール)
※プロジェクトが別ならバージョンも分ける
標準ライブラリ以外のコードはモジュールとして宣言する
※すべてのモジュールはグローバルにユニークな識別子を持つ
※モジュールのあるリポジトリへのパスを使う
※github.com/${UserName}/${ModuleName}
※https://go.dev/doc/modules/managing-dependencies#naming_module
---
go mod init MODULE_PATH
※MODULE_PATHの大文字・小文字は区別されるが、小文字を使う
---
---
module MODULE+PATH
go 1.19
require (
github.com/xxx/yyy v1.0.0
github.com/zzz v2.1.0
)
---
※1.19以上が必要
---
go mod tidy
---
※外部モジュールのダウンロードなどを行ってくれる
import
※他のパッケージでエクスポートされた定数、変数、型、関数、メソッド、構造体のフィールドにアクセスできる
※エクスポートする場合は先頭が大文字で後方互換性を考慮する必要がある
※importする際はディレクトリまで
※相対パスは使わない
※絶対パスが明確
※絶対パスがリファクタリングが簡単
※相対パスで他のモジュールに移動した場合、インポートを絶対パスにする必要が出てくる
※ディレクトリが同じ場合はパッケージ名を別名でオーバーライドする(例: crand "crypto/rand")
※パッケージ名と同じ変数、型、関数を宣言すると、パッケージにアクセスできなくなるが、その場合はパッケージ名を別名でオーバーライドする
※パッケージ名はファイルブロックにある
※ファイル内で必要なものは自ファイル内でimportする(同じディレクトリの他のファイルがimportしていても)
※import . "xxx/yyy"は使わない
※.を使うとそのパッケージのすべてのエクスポートされた識別子が現在のパッケージの中に置かれる
※.を使うと参照するのにプレフィックスを使わなくても使えてしまう
※.を使うとパッケージで定義されたのかインポートされたのかわかりにくくなる
※import _ "xxx/yyy"でパッケージ内のエクスポートされた識別子にアクセスできなくなる
※_を使うと識別子が使われなくてもエラーにならないので使わない
※未使用のimportはコンパイラが指摘してくれる
package
※パッケージ名はインポートパスではなく、パッケージ節で決まるが、パッケージ名はディレクトリ名に合わせる
※例外はpackage main(importできない)
※使う際は${パッケージ名}.${メソッド名}
※util等ではなく、内容(提供される機能)を表現するように付ける
※util.ExtractNamesとutil.FormatNamesではなく、extract.Namesとformat.Namesにする
※パッケージの名前をそのパッケージ内の関数や型の名前で繰り返すのは避けるべき
※extract.ExtractNamesではなく、extract.Names
※例外は識別子とパッケージ名が同じ場合: sort.Sort, context.Context
モジュールが小さいときはひとつのパッケージにすべてのコードを入れる
※他に依存するモジュールがない場合は構成はきちんと決めなくても問題は起こらない
複数のアプリケーションでモジュールが構成される場合
※モジュールのルートにcmdディレクトリを作成する(例: データベース内のデータ分析用cmd/apps/main.go,cmd/tools/main.go)
※cmd/apps,cmd/toolsはそれぞれpackage mainにする
※main以外のGoのコードはpkg下に置く
※pkg下はパッケージ間の依存関係が小さくなるように、機能のまとまりごと(例: pkg/customer,pkg/inventory)にコードを置く
※あとで複数のマイクロサービスにリファクタしやすいようにする
※How Do You Structure Your Go Apps: https://www.youtube.com/watch?v=oL6JBUk6tj0
※Standard Go Project Layout: https://github.com/golang-standards/project-layout/blob/master/README_ja.md
パッケージコメント
※パッケージ用に長いコメントを書く場合はdoc.goを置く
※コメントのリントツール(golint, golangci-lint)
---
go doc パッケージ名
go doc パッケージ名.識別子
---
親パッケージと兄弟パッケージからしか使われないパッケージはpackage internalにする
initは他のパッケージから最初に参照されたときに自動的に実行される
※パッケージレベルで変数を初期化して、ミュータブルな状態をもつのはよくない
※Goでは値が変わらないことを保証する方法を提供しない
※実行中に変更されるパッケージレベルの変数は使わず、パッケージ内の関数で初期化された構造体の中に置くべき
※1つのパッケージで複数init関数を書かない
循環参照
※エラーになる
※循環参照のエラーが発生したら、ひとつのパッケージにするか、原因となった項目を別のパッケージに移動か別のパッケージを作成する
型のエイリアス
---
type T2 = T1
---
※T1の変数に型変換なしで代入できる
※用途はAPIの後方互換性を考えた段階的な変更
※メソッドの追加やフィールドの変更はT1で
※他のパッケージの型にエイリアスを付けると、エクスポートされていないメソッドやフィールドにはエイリアスで参照できない
※パッケージレベルの変数やエクスポートされていても構造体のフィールドにはエイリアスを付けることができない
モジュールのバージョン確認
---
go list -m -versions github.com/xxx/yyy
---
※モジュールのアップデートはgo get github.com/xxx/yyy
※go getでgo.modのバージョンも更新される
ベンダリング
---
go mod vendor
---
※Goモジュールとプロキシサーバの発展に伴い、使われなくなっている
https://pkg.go.dev/
※Goモジュールのドキュメントサイト
モジュールの公開
※中央集権的なリポジトリにモジュールをアプロードする必要はない
※LICENSEファイルを置く
※https://itsfoss.com/open-source-licenses-explained/
※セマンティックバージョンに従ってタグ付けをする
※後方互換性に影響がある場合はv2ディレクトリ作成とv2ブランチの作成とimportの末尾にv2
モジュールプロキシサーバー
※https://proxy.golang.org/
※パブリックなすべてのGoモジュールの全てのバージョンのコピーがある
※チェックサムデータベースによる改ざんされたモジュールのインストール防止
※誤って修正、機能追加を既存のバージョンタグでしないように
並行処理
CSP: Communicating Sequential Processes
※https://dl.acm.org/doi/pdf/10.1145/359576.359585
並行処理を使う条件
※独立している複数の処理
※並行にしないと処理が遅い
※並行処理のオーバーヘッドを引いても処理が速くなる
※処理がディスクまたはネットワークを介した読み書き(インメモリではない処理)
プロセス
※OSによって実行されているプログラム
※OSはプロセス毎にリソースを排他的に制御する
スレッド
※プロセス内の処理の実行単位
※スレッド同士はリソースを共有
※1プロセスにはコア数だけスレッドを実行できる
Goランタイム
※プログラム実行時に複数のスレッドを作成する(1つのスレッドに割り当てられる1つのgoroutineを含む)
goroutine
※Goランタイムに管理されるスレッド
※全体が同一プロセス内で実行される
※OSのスレッドの生成よりも速い
※スタックサイズは可変
※スイッチングがスレッドよりも速い
※スケジューラがI/Oををブロックしているgoroutineをスケジューラの対象から外すときを検知できる
※GCとも統合されている
※何百、何千、何万のgoroutineを同時に起動でき、速く実行することができる
※The Scheduler Saga: https://www.youtube.com/watch?v=YHRO5WQGh0k
go func
---
func runThingsConcurrently(chIn <- chan int, chOut chan<- string) {
for val := range chIn { // chInから値が到着するたびに
go func(val int) {
result := doBusinessLogic(val)
resultString := fmt.Sprintf("%d -> %d", val, result)
chOut <- resultString // 結果をchOutに入れる
}(val)
}
}
---
※任意の関数名の前にgoを置くとgoroutineになる
※ビジネスロジックをラップするクロージャとともに起動する
※https://go.dev/tour/concurrency/1
チャネル
---
ch := make(chan int) // バッファリングされない
ch := make(chan int, 10) // バッファリング
a := <-ch // チャネルからの読み込み
ch <- b // チャネルへの書き込み
ch <- chan int // 受信専用チャネル(読み込みのみ)
ch chan<- int // 送信専用チャネル(書き込みのみ)
for v := range ch {
fmt.Println(v)
}
// チャネルがクローズされたか確認
v, ok := <- ch
---
※goroutineでデータのやり取りに使う
※他言語ではミュータブルでグローバルに共有された状態に依存してスレッド間で情報をやり取りする
※チャネルのゼロ値はnil
※関数にチャネルを渡すとき、チャネルへのポインタが渡される
※1つのgoroutineが同じチャネルに読み書き両方を行わない
※基本的にデフォルトのバッファリングされないチャネルを使う
※書き込みは同じバッファリングされないチャネルに他のgoroutineが読み込むまで一旦停止
※読み込みは同じバッファリングされないチャネルに他のgoroutineが書き込むまで一旦停止
※バッファ内の値はlen, バッファの最大サイズはcapで
select
---
select {
case v := <-ch:
case v := <-ch2:
case ch3 <- x:
case <-ch4
}
---
※各caseは独自ブロック
※データの準備ができているcaseのうちからランダムに実行
※チャネルでのやりとりはselectで囲うことでデッドロックを防ぐ
デッドロック
---
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)
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
v := 1
v2 := <- ch1 // ch1からの読み込み
ch2 <- v // ch2への書き込み
fmt.Println(v, v2)
}()
v := 2
v2 := <-ch2 // ch2からの読み込み
ch1 <- v // ch1への書き込み
fmt.Println(v, v2)
}
---
※ch1, ch2の順でチャネルにアクセスしていない
※ch1, ch2の順でアクセスするか、go func以外のchへのアクセスをselectで囲めばデッドロックしない
for-select
---
for {
select {
case <- done:
return
case v:= <-ch:
fmt.Println(v)
}
}
---
※defaultを置くと無限ループになる
並行処理のベストプラクティス
※公開するAPIの型、関数、メソッドにチャネルは含めない
---
a := []int{2, 4, 6}
ch := make(chan int, len(a))
for _, v := range a {
go func(val int) {
ch <- val * 2
}(v)
}
---
※goroutineの外側の変数には依存させず、引数で渡す
goroutineは確実に終了させる
---
func searchData(s string, searchers []func(string) []string) []string {
done := make(chan struct{}) // チャネルの型は空の構造体(3章参照)
resultChan := make(chan []string)
for _, searcher := range searchers {
go func(f func(string) []string) {
select {
case resultChan <- f(s):
fmt.Println("結果が戻ってきた")
// クローズ時の処理
case <-done: // closeされるまでポーズ(クローズされたチャネルからは常にゼロ値)
fmt.Println("doneを選択")
}
}(searcher) // 無名関数の引数。関数をひとつずつ渡す
} // for の終わり
r := <-resultChan // 最も速かった結果
close(done) // チャネルdoneをクローズ
return r // 文字列のスライス
} //listend1
---
※Goのランタイムは使われていないgoroutineを検知できない
※goroutine leakを防ぐ
---
func countTo(max int) (<-chan int, func()) { //liststart
ch := make(chan int)
done := make(chan struct{})
cancelFunc := func() { // chをクローズする関数を戻す
close(done)
}
go func() {
for i := 0; i < max; i++ {
select {
case <-done: // close(done)が呼ばれたときchのクローズ
return
case ch <- i:
}
}
// chのクローズ
close(ch)
}()
return ch, cancelFunc
}
func main() {
ch, cancelFunc := countTo(10)
for i := range ch {
if i >= 5 { // 途中で抜けたくなったら(抜ける条件が整ったら)
break
}
fmt.Print(i, " ")
}
fmt.Println()
cancelFunc() // chをクローズしてforループを終了する
} //listend
---
※chをクローズする関数を戻して呼び出し側で実行する
※close(done)が実行されたらclose(ch)されるようにselectにcase <-doneでreturnしてforを抜ける
起動するgoroutineの数を制限したい
※バッファ付きのチャネルを使う
---
func processChannel(ch chan int) []int {
const maxConc = 10
results := make(chan int, maxConc)
for i := 0; i < maxConc; i++ {
go func() {
v := <- ch
results <- process(v)
}()
}
fmt.Println("ゴルーチン 起動完了")
var out []int // intのスライス
for i:=0; i<maxConc; i++ {
out = append(out, <-results) // 結果を受け取って後ろに追加
}
return out
}
---
バッファに入ったもの処理に制限をかけたい
※バッファ付きのチャネルを使う
※処理の量を管理して、遅くなるのを防ぐ場合
---
// pressure gaugeは「圧力計」のこと
type PressureGauge struct {
ch chan struct{}
}
func New(limit int) *PressureGauge {
ch := make(chan struct{}, limit)
for i := 0; i < limit; i++ {
ch <- struct{}{}
}
return &PressureGauge{
ch: ch, // 満杯になっているチャネルを戻す
}
}
// PressureGauge pgを受け取って、それが許す範囲で関数fを実行する
func (pg *PressureGauge) Process(f func()) error {
select {
case <-pg.ch:
f() // fを実行する
pg.ch <- struct{}{} // ひとつ余裕ができた。空の構造体のインスタンスを送る
return nil
default:
return errors.New("キャパシティに余裕がありません")
}
}
func doThingThatShouldBeLimited() string {
time.Sleep(2 * time.Second)
return "done"
}
func main() {
pg := New(10) // PressureGaugeの初期化。最大数を10個にする
http.HandleFunc("/request", func(w http.ResponseWriter, r *http.Request) {
err := pg.Process(func() { // 無名関数をpgの範囲で処理
w.Write([]byte(doThingThatShouldBeLimited())) // バイトのスライスに型変換
})
if err != nil {
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte("Too many requests"))
}
})
fmt.Println("ブラウザで次を開いてください: 'http://localhost:8080/request'")
fmt.Println("あるいは 'sh ex1010.sh' を実行してみてください")
http.ListenAndServe(":8080", nil)
}
---
チャネルがnilになる場合を考慮する
※nilチャネルに対する読み書きはハングアップしてしまう
※チャネルがクローズされたら、チャネルの値をnilにしてcaseが実行されないようにする
---
for {
select {
case v, ok := <-in:
if !ok {
in = nil
continue
}
case v, ok := <-in2:
if !ok {
in = nil
continue
}
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 { // doneが閉じられるか
case <-done:
return result, err
case <-time.After(2 * time.Second): // 2秒経過してしまうとこちら
return 0, errors.New("タイムアウトしました")
}
}
---
複数のgoroutineの終了を待つ場合
---
func main() {
var wg sync.WaitGroup // 宣言だけでゼロ値を使う
wg.Add(3) // 終了を待つカウンタに3を設定
// すべての場所で同じインスタンスが使われるようにwgは渡さない
go func() {
defer wg.Done() // カウンタを-1
doThing1()
}()
go func() {
defer wg.Done()
doThing2()
}()
go func() {
defer wg.Done()
doThing3()
}()
wg.Wait() // カウンタが0になるまで、goroutineをポーズ
}
---
---
func processAndGather(processor func(int) int, data []int) []int {
num := len(data)
chResult := make(chan int, num) // 処理結果を受け取るチャネル
// dataのサイズと同じ長さのバッファをもつチャネルを作る
var wg sync.WaitGroup
wg.Add(num)
for _, v := range data {
go func(v int) {
defer wg.Done() // 処理が終わったらwgをデクリメント
chResult <- processor(v) // 処理結果をチャネルに入れる
}(v)
}
wg.Wait() // すべてのゴルーチンが処理を終了するのを待つ
close(chResult)
var result []int
for v := range chResult { // chResultのバッファに残っている要素を処理
result = append(result, v)
}
return result
}
func main() {
inpValues := []int{1, 2, 3, 4, 5}
outValues := processAndGather(
func(j int) int {return j*j}, // 第1引数は関数
inpValues) // 第2引数は計算する値(intのスライス)
fmt.Println(outValues)
}
---
1つでもエラーになったら処理を停止したい場合
https://pkg.go.dev/golang.org/x/sync/errgroup
時間のかかる処理を一度だけ実行したい
---
// 新しいインスタンスが作成されないようにパッケージレベルで宣言する
// 関数内で宣言すると関数呼び出しの度にインスタンスのコピーが作られる
var once sync.Once
func Parse(dataToParse string) string {
// 1回目の呼び出し時だけ実行される
once.Do(func() {
// 時間のかかる処理
parser = initParser()
})
return parser.Parse(dataToParse)
}
---
コンテキストがある場合のタイムアウト実装
ctx, cancel := context.WithTimeout(ctx, timeLimit*time.Millisecond)
defer cancel() // タイムアウト時にキャンセルされる
※ctxはcontext.Context
※コンテキストがある場合はtime.Afterではなく、コンテキストを使う
mutex
※データの流れが不明確
※データに対するアクセスは並列に実行されているプロセスのすべてによって共有される
※データの所有権をもっているgoroutineがどれか不明になる
※メモリの共有でコミュニケーションすることになる
※使うときは複数のgoroutineが共有されたデータを読んだり、書いたりするが、値を処理しないもの
※複数のgoroutineからひとつの変数にアクセスする場合に使う(sync.Mapではなく、sync.RWMutex)
※ゲームのスコアボード等で使う
※ロックの取得・解除を正しくしないと、デッドロック
※データが外部サーバーやDBに保存される場合のアクセス保護にmutexは使わない
※コピーするとロックが共有されない
※関数に渡されたり、構造体のフィールドとしてアクセスされる際にはポインタを使う
チャネルかmutexか
※goroutineの関係を調整しようとしている: チャネル
※一連のgoroutineによって変換されるデータをトラックしている: チャネル
※構造体のフィールドを共有: mutex
※チャネルで致命的なパフォーマンスの問題がある: mutex
sync/atomic
※複数スレッドでのデータの一貫性を保つ
※ひとつのレジスタに十分入る値に対する加算、スワップ、ロード、ストア、比較とスワップに使う
※並行実行のエキスパートが使うもの
標準ライブラリ
入出力
アプリケーション構築に必要なものは「すぐに使えるようにすべて含める」というGoの哲学
標準ライブラリ: https://pkg.go.dev/std
io.ReaderのRead
※データがあるか確認してから、エラーチェックをするので、通常とは逆でエラーを後でチェックする
※エラー: io.EOF, io.ErrUNexpectedEOF
---
f, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer f.Close()
---
※ループの中でリソースをオープンしている場合はdeferを使わない
※ループの繰り返しの終わりでCloseを呼ぶ
※エラーがあった場合は、そこでもCloseを呼び出す必要がある
Goのioパッケージのメソッドを図示: https://christina04.hatenablog.com/entry/golang-io-package-diagrams
os.Fileではなく、引数の型に自分の関数が行うことを具体的に示すインターフェイスを指定する
※例: io.ReadCloser, io.WriteCloser, io.ReadWriteCloser, io.ReadWriteSeeker, io.ReadWriter, io.WriteCloser, io.WriteSeeker
※意図が明確になる
※独自のデータソースや結果ファイルを記述している場合にコードが互換性をもつようになる
小さいデータの場合
※io.ReadAll, ip.ReadFile, io.WriteFile
大きいデータの場合
※bufioのReader, Writer, Scanner
時間
time.Duration
※時間を表す
※d := 2 * time.Hour + 30 * time.Minute + 45 * time.Second
※文字列との変換はtime.ParseDuration(関数)を使う
※String, Trancate, Roundメソッドあり
※サマータイム、うるう秒、NTPアップデートは対応済み
time.Time
※時刻
※time.Nowは現在時刻のtime.Timeインスタンスが返される
※比較にはBefore, Equal, Afterメソッドを使う
※stringからの変換はtime.Parse
※差分はSubメソッド
※時刻の加減はAddメソッド
※サマータイム、うるう秒、NTPアップデートは対応済み
並行実行のタイムアウトや繰り返し
※time.Afterは出力するチャネルを戻す
※time.AfterFuncで指定時間後に関数を起動できる
※time.Tickのベースのtime.Tickerはシャットダウンできず、GCされない
※time.NewTickerはリッスンするチャネルやティッカをリセットしたり、ストップするメソッドをもった*time.Tickerを返す
json
マーシャリングとは、異なる技術基盤で実装されたプログラム間で、データ形式の変換などを行うこと。
func Unmarshal(data []byte, v any) error
※jsonをstructに変換する
※structのフィールド名は大文字から始める
※encoding/jsonのコードは、他のパッケージ内の構造体のエクスポートされていないフィールドにはアクセスできない
※vはポインタ(&s)
※vはポインタであることで同じ構造体を繰り返し用いて、io.Readerと同様に、効率よくメモリ利用される
※構造体の型の後には`json:"field_name"`
Goでは「明示的でわかりやすいコード」が「短いコード」よりも好まれます
func Marshal(v any) ([]byte, error)
※vはポインタではなく、構造体を渡す
io.ReadAllを使って、データの全体をコピーし、json.Unmarshalするのは効率がよくない
---
func main() {
data := readData()
var o Order
err := json.Unmarshal(data, &o)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", o) // %+v でフィールド名付きで出力
}
func readData() []byte {
file, err := os.Open("testdata/data.json")
if err != nil {
log.Fatal(err)
}
defer file.Close()
bytes, err := io.ReadAll(file) // ファイルの内容をすべて読み込み
// closeもやってくれるので、closeの必要もなし
// (もっともcloseの対象が返ってこないので、closeしようがない..)
if err != nil {
log.Fatal(err)
}
return bytes
}
---
json.Marshalでバッファ([]byte)に書き込んだデータをネットワークやディスクに書き込むよりio.Writer(os.File)で直接書いた方がよい
---
func main() {
file, err := os.Open("testdata/data.json")
// func Open(name string) (*File, error)
if err != nil {
log.Fatal(err)
}
defer file.Close()
var o Order
d := json.NewDecoder(file) // 下記コメント参照
d.Decode(&o)
fmt.Printf("%+v\n", o)
}
---
複数の構造体の読み書き
※json.Decoderを使って、配列全体をメモリにロードすることなく、配列から単一のオブジェクトを読み込むことができる
※パフォーマンスが大幅に向上し、メモリ使用量も削減できる
https://pkg.go.dev/encoding/json#example-Decoder.Decode-Stream
独自型を定義して、MarshalJSONとUnmarshalJSONを上書きする
---
type RFC82ZTime struct {
time.Time
}
type Order struct {
ID string `json:"id"`
Items []Item `json:"items"`
DataOrdered RFC82ZTime `json:"date_ordered"`
CustomerID string `json:"id"`
}
---
※JSONの日付のフォーマットがデータ構造のフィールドに影響してしまうのが欠点
※OrderにMarshalJSONとUnmarshalJSONを上書きするとすべてのフィールドを処理するコードを書かないといけなくなる
※JSONの変換用とデータ処理用の2つの構造体を定義する方法もある
Goの標準ライブラリには他の言語がサードパーティーの責任と考えていたHTTP/2のクライアントとサーバーが含まれている
net/http
※DefaultClientはタイムアウトがないので、本番用では独自のインスタンスを生成する
---
client := http.Client{
Timeout: 30 * time.Second,
}
---
※リスエストを送りたいときはhttp.NewRequestWithContext
---
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
url,
nil // PUT, POST, PATCHはio.Readerでボディを指定(この場合はなし)
)
---
※ヘッダーの追加
---
req.Header.Add("X-My-Client", "Learning Go") //liststart3
---
※リスエスト送信
---
res, err := client.Do(req)
defer res.Body.Close() //liststart4
---
※ステータスコード確認
---
if res.StatusCode != http.StatusOK {
}
---
※結果を構造体にあてる
---
var data struct {
UserID int `json:"userId"`
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
err = json.NewDecoder(res.Body).Decode(&data)
---
http.Server
※TLSサポートのパフォーマンスの高いHTTP/2サーバー
※実装はhttp.ServerのフィールドHandlerのインターフェイス(http.Handler)の実装
※http.Serverは1つのHandler指定しかできないので、リクエストルータhttp.ServeMuxを使う
※mux: multiplexer(Goではパスとハンドラーを管理する機構)
---
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
type ResponseWriter interface {
Header() http.Header()
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
---
※Header, WriteHeader(200の場合は不要), Writeの順で呼び出す
http.ServeMux
---
personMux := http.NewServeMux() //liststart1
personMux.HandleFunc("/greet",
func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("こんにちは!\n"))
})
dogMux := http.NewServeMux()
dogMux.HandleFunc("/greet",
func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("waon \n"))
})
mux := http.NewServeMux()
mux.Handle("/person/", http.StripPrefix("/person", personMux))
mux.Handle("/dog/", http.StripPrefix("/dog", dogMux))
log.Fatal(http.ListenAndServe(":8080", mux))
---
※http.Handlerではなく、*http.ServeMuxのHanderFuncを実装する
※HandleFuncは引数の関数をhttp.HandlerFunc型に変換する
※先頭の/はhttp.StripPrefixで除く
本番環境
※http.Handle, http.HandleFunc, http.ListenAndServe, http.ListenAndServeTLS, http.Serverは使わない
※上記はhttp.DefaultServeMuxで実行されるため
複数のハンドラで共通処理をしたい場合
※http.Handlerのインスタンスを引数で受け取り、http.Handlerを返す関数でネストさせる
※ネストさせたくたい場合はaliceを使う
※alice: https://github.com/justinas/alice
---
mux.Handle("/hello", terribleSecurity(RequestTimer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!\n"))
})))) //listend3
---
標準HTTPライブラリ(*http.ServeMux)の欠点
※HTTPの動詞やヘッダに基づいてハンドラを指定できず、URLパス内の変数にも対応していない
※*http.ServeMuxのネスト
サードパーティーモジュール
※gorilla/mux: https://github.com/gorilla/mux(The Gorilla Toolkit is now in archive-mode, and is no longer actively maintained.)
※chi: https://github.com/go-chi/chi
※メンテナンスされなくなる可能性大
コンテキスト
他の言語ではリクエストを識別して、タイムアウトを設定するためにスレッドローカル変数を使う
GoのgoroutineはIDを持っていないので、コンテキストを使う
コンテキストはAPIの境界を超えて(あるいはプロセス間)以下のリクエストのメタデータを保持する
・処理のデッドライン(time.Time)
・キャンセレーションシグナル
・その他の処理に必要な値
コンテキストは関数の最初の引数で渡す慣習
(ctx context.Context)
コンテキストがない場合は空のコンテキストを生成する
ctx := context.Background()
httpにコンテキストを追加する
ctx := req.Context()
req = req.WithContext(ctx)
キャンセルはエラーが発生したときに他のgoroutineの処理を中止するために使う
ctx, cancel := context.WithCancel(ctx)
※ctxはラップされた子コンテキストが返る
defer cancel()
※最初の呼び出し以外は無視されるが、必ず呼び出されるようにしないとリソースをリークしてしまう
※他のHTTPサービスやデータベースを呼び出すときは必ずコンテキストを渡すことでキャンセルの処理が行われる
go func内では以下
if err != nil {
cancel()
}
同時リクエスト数の制限: goroutineの数を制限する
実行を待つキューに入るリクエスト数の制限: バッファ付きチャネルで制限する
リクエストの実行時間の制限: コンテキスト
parentCtx, parentCancel := context.WithTimeout(ctx, 2 * time.Second)
defer parentCancel()
childCtx, childCancel := context.WithTimeout(parentCtx, 3 * time.Second)
defer childCancel()
<-childCtx.Done()
※約2秒
※Done()はstruct{}を返す
※タイムアウト or キャンセルされるとチャネルがクローズされ、ゼロ値が返る
※キャンセルできないコンテキストをDone()するとnilが返り、nilチャネルからの読み込みはできないので、selectしないとハングアップする
JWTからユーザーを抽出したり、リクエスト毎にGUID(Globally Unique Identifier)をしたい場合はコンテキストに値を入れる
ctx := req.Context()
val, ok := ctx.Value(key).(string)
ctx = context.WithValue(ctx, key, value)
req = req.WithContext(ctx)
テスト
エクスポートされていない関数や変数にもアクセスしてテストできるようにするため、テストコードをコードと同じディレクトリ・パッケージに置く
xxx.go
xxx_test.go
func Test_yyy(t *testing.T) {
result := yyy()
if result != {
e.Errorf()
}
}
// _testでパッケージのエクスポートものだけ使うことができる
package ${file}_test
import (
"testing"
// importが必要
"${dir}/${file}"
)
func TestZzz(t *testing.T) {
result := Zzz()
if result != {
e.Errorf()
}
}
テストが成功してコードが変更されていなければ結果はキャッシュされる
go test
go test -v
go test -count=1 // 強制再実行
go test -bench. -benchmem
go test -short
go test -race -trimpath
go test -cover
go tool cover -html=c.out -o coverage.html
func TestMain(m *testing.M) {
....
exitVal := m.Run()
....
os.Exit(exitVal)
}
※テスト毎ではなく、1回だけ
※1パッケージでTestMainは1個だけ
後処理
t.Cleanup(func() {
os.Remove(f.Name())
})
テストデータ用ディレクトリ
testdata
合成型のインスタンスのテスト
github.com/google/go-cmp/cmp
if diff := cmp.Diff(expected, result); diff != nil {
t.Error(diff)
}
ベンチマーク
速さとメモリ消費がビジネス上の要求を満たしている場合には最適化よりも機能追加すべき
func BenchmarkXxx(b *testing.B) {
for i := 0; i < b.N; i++ {
...
}
}
※Nは妥当な結果を得るまでフレームワークが裏側で増やしながら実行
Profiling Go programs with pprof: https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/
go fix
※go.modのバージョンに合った書き方に修正してくれる
if testing.Short() {
t.Skip("shortモードでスキップ")
}
リフレクション
リフレクションは外部とのやり取りに使われる
※遅いので、プロダクトコードでは使わない
型を調べたい
vType := reflect.TypeOf(v)
VType.Name()
vType.Kind()
ポインターの値の型を調べたい
pType := reflect.TypeOf(p)
pType.Elem().Name()
pType.Elem().Kind()
インターフェイスのnilチェック
※具象型のnilの変数をインターフェイス型の変数に代入するとインターフェイス型の変数はnilではない
iv := reflect.ValueOf(i)
if !iv.IsValid() {
// インターフェイスはnil以外
}
ジェネリクス(総称型)
ユーザーが定義した型や関数でもコンパイル時に型チェックをする
複数の型に対応するデータ構造を定義する
ジェネリクスを使わず、関数の引数にanyを使うとコンパイル時にチェックできず、実行時にエラーになる
type Stack[T any] struct {
vals []T
}
var intStack Stack[int]
var stringStack Stack[string]
※Tは名前
※anyではなく、comparableや他のインターフェイスも可
ジェネリクスは処理が遅くなる可能性がある