本ツールで使用しているオープンソースライブラリ

本ツールのコードには 1 個のオープンソースライブラリが含まれています。

Go チートシート — クイックリファレンス

Go 1.22 の構文、型、goroutine、頻繁に使う標準ライブラリをまとめたクイックリファレンスです。日常の 80% ほどをカバーします。

Go

Go Go 1.22

Go (gc toolchain) · コンパイル型・並行・命令型 · 静的・強い型付け・構造的型付け(インターフェース)

おすすめの学習パス

まずは go run / go build と go.mod の基本を覚え、続いて変数・型・制御フローを学びましょう。次に関数・ポインタ・構造体を深く扱い、スライスとマップでデータを整理します。インターフェースとエラー処理を理解したら、goroutine とチャネルで並行処理を書けるようになりましょう。必要に応じて http、context、時間、ビルド/テストを学んでください。落とし穴を避けるには FAQ 章を後で見返すのがおすすめです。

1.Hello World とビルド環境

Go プログラムの実行、go.mod モジュール、ツールチェーンについて説明します。

最小プログラム

すべてのプログラムは package main と func main をエントリーポイントとします。fmt で出力します。

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
// 実行:
// go run hello.go
// package main は実行可能プログラムを生成
// 他の package はライブラリ
// main 関数は戻り値を持たない

実行とビルド

go run でコンパイルして実行、go build で実行ファイルを生成、go install で $GOBIN にインストールします。

1
2
3
4
5
6
7
8
9
10
11
12
// 直接実行:
// go run main.go
// パッケージ全体を実行:
// go run .
// 実行ファイルを構築:
// go build -o hello .
// $GOBIN にインストール:
// go install .
// コンパイル検査:
// go vet ./...
// 出力バイナリ名は既定でモジュール名
// Windows では .exe を生成

go.mod モジュール

go.mod はモジュールパスと依存関係を宣言します。go mod init で初期化、go mod tidy で整理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// モジュールの初期化:
// go mod init example.com/hello
// go.mod の内容:
// module example.com/hello
// go 1.22
// 依存の追加:
// go get github.com/gin-gonic/gin
// 依存の整理:
// go mod tidy
// 依存の更新:
// go get -u ./...
// 依存キャッシュ:
// すべてのモジュールは GOPATH/pkg/mod にある
// 検証:go mod verify

パッケージとインポート

import で標準ライブラリとサードパーティを取り込みます。同じディレクトリのファイルは同じパッケージです。未使用のインポートはコンパイルエラーになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"os"
"strings"
)
// サードパーティをインポート:
// import "github.com/gin-gonic/gin"
// エイリアス:
// import f "fmt"
// 空インポート(init を発火):
// import _ "net/http/pprof"
// 未使用のインポートはコンパイル失敗:
// _ で抑制する
// パッケージ名はインポートパスの最後のセグメントと一致

フォーマット出力

fmt.Println/Printf/Print を使用します。Printf の動詞は %s、%d、%v などです。Sprintf は文字列を返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
fmt.Println("Hello") // 自動で改行
fmt.Printf("Age: %d\n", 30)
fmt.Printf("%s is %d\n", "Nick", 30)
// よく使う動詞:
// %v デフォルト %#v 構造体 %T 型
// %d 整数 %f 浮動小数点 %.2f 2 桁
// %s 文字列 %q 引用符付き
// %t ブール %x 十六進数
// 文字列を返す:
s := fmt.Sprintf("sum: %d", 1+1)
}

引数と入力

os.Args でコマンドライン引数にアクセスし、flag で解析、fmt.Scan で入力を受け取ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"os"
)
func main() {
// os.Args[0] はプログラムのパス
args := os.Args[1:]
fmt.Println("参数:", args)
// 入力を読み取る:
var name string
fmt.Scanln(&name) // 1 行読み取る
// プロンプト付き:
fmt.Print("输入: ")
var n int
fmt.Scan(&n)
}

複数ファイルプログラム

同じディレクトリの複数の .go ファイルは同じパッケージに属し、パッケージスコープを共有します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// utils.go
package main
func helper() string {
return "hi"
}
// main.go
package main
import "fmt"
func main() {
fmt.Println(helper()) // 直接呼び出し
}
// 実行:
// go run .
// 同じパッケージの識別子は相互に使える
// 関数名が小文字ならパッケージ内プライベート
// 大文字でパッケージ外(エクスポート)

ツールチェーン

go version でバージョン、go env で環境変数、go doc でドキュメント、go fmt でフォーマットを確認できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// バージョン:
// go version
// 環境変数:
// go env GOPATH GOROOT
// go env GOPROXY # プロキシ
// ドキュメント:
// go doc fmt.Println
// go doc strings.Builder
// フォーマット:
// gofmt -w .
// 依存グラフ:
// go list -m all
// ツールチェーンの更新:
// go get golang.org/dl/go1.22.0
// ヘルプ:go help <コマンド>

2.変数と定数

変数宣言、短変数宣言、定数、ゼロ値、型変換を扱います。

変数宣言

var で変数を宣言し、型は変数名の後に書きます。初期化するかゼロ値を与えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name string
var age int
var ok bool
// 初期化付き:
var count int = 5
var title = "Go" // 型推論
// 型は名の後に置く:
var score float64
// 一括宣言:
var (
a int
b string
)
// 初期化されていない変数はゼロ値
// 宣言して使わないとコンパイル失敗

短変数宣言

:= は型推論付きで宣言と初期化を行います。関数内でのみ使用でき、左辺に少なくとも 1 つの新しい変数が必要です。

1
2
3
4
5
6
7
8
9
10
11
name := "Go" // string と推論
count := 42
flag := true
// 既存変数 + 新変数:
name, age := "Nick", 30
age = 31 // 既存なら = を使う
// 再宣言:
// := は左辺に少なくとも 1 つ新変数が必要
// 関数の外では := を使えない
// スコープ内での重複宣言はエラー
// x, y := 1, 2

定数

const はコンパイル時に確定する定数を宣言します。iota で自動増分の列挙を定義できます。定数は任意の精度を持ちます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Pi = 3.14
const (
StatusOK = 200
StatusNotFound = 404
)
// iota で自動増加:
const (
A = iota // 0
B // 1
C // 2
)
// 型付き定数:
const MaxSize int = 100
// 無型定数は任意の型に使える:
const N = 10
var f float64 = N // 合法
// 定数は変更できない

ゼロ値

未初期化の変数はゼロ値を持ちます:数値は 0、文字列は空文字、bool は false、ポインタは nil です。ゼロ値はそのまま使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var n int // 0
var s string // ""
var b bool // false
var p *int // nil
var arr [3]int // [0 0 0]
var m map[string]int // nil map
// ゼロ値はそのまま使える:
var buf bytes.Buffer
t := buf.String() // ""
// nil スライスは append できる:
var nums []int
nums = append(nums, 1)
// nil マップには書き込めない:
// m["a"] = 1 // panic
// 構造体のゼロ値:var r bytes.Reader

多重代入

1 行で複数の変数に代入でき、値の交換もできます。関数の複数の戻り値も分解して受け取れます。

1
2
3
4
5
6
7
8
9
10
11
a, b := 1, 2
a, b = b, a // 交換
// ある値を無視:
x, _ := getPair()
// 複数戻り値のアンパック:
value, err := risky()
// 部分更新:
map1["a"], n := "x", 1
// 同時に初期化:
// ただし := は少なくとも 1 つ新変数が必要
// 関数の戻り値のアンパック数は一致必須

スコープ

ブロックスコープが基本です。関数内の変数はブロック内でのみ有効です。シャドーイングに注意してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
var global = 10 // パッケージレベル
func f() {
x := 1
if y := x + 1; y > 0 {
z := y * 2 // if ブロック内
_ = z
}
// y、z はここでは見えない
}
// 遮蔽:
func g() {
global := 99 // パッケージレベルの global を遮蔽
_ = global
}
// 短い宣言は外側の変数を遮蔽する
// ループ変数はブロックスコープ

型変換

T(x) で明示的に変換します。Go は数値型を暗黙的に変換しません。変換ではオーバーフローや切り捨てが発生することがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var i int = 42
f := float64(i) // int -> float64
// 浮動小数点から整数への変換は切り捨て:
n := int(3.99) // 3
// 文字列から数値へ:
// strconv を使う:
s := strconv.Itoa(42) // "42"
n2, _ := strconv.Atoi("42")
// バイトから文字列へ:
s2 := string([]byte{'h', 'i'})
// 注意:string(65) は "A" で "65" ではない
// rune 変換:
r := rune(20013)
// オーバーフローは切り捨て:
// var b byte = 300 コンパイルエラー

命名規約

先頭が大文字の識別子はパッケージ外に公開され、小文字は非公開です。キャメルケースを使用し、ローカル変数は短くします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package shapes
// エクスポート(先頭大文字):
export const Area = 100
func Calculate() float64 // エクスポート
// プライベート(小文字):
func helper() int // パッケージ内
var internalCount = 0
// キャメルケース:
// maxRetries / getUserById
// 頭字語は大文字のまま:
// theURL / HTTPClient
// 推奨:
// 短い名前はローカル変数に使う(i、n)
// パッケージ名はすべて小文字の単数形

3.データ型

組み込み型、構造体、配列、インターフェース、ジェネリクスについて説明します。

基本型

bool、string、整数、浮動小数、複素数、byte/rune のエイリアスがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var b bool
var s string
// 整数:
var i int // プラットフォーム依存
var i8 int8; var u uint
// 浮動小数点:
var f32 float32; var f64 float64
// 複素数:
var c complex128
// 別名:
var by byte = 'a' // uint8
var r rune = '中' // int32
// 符号なし:
uint8 uint16 uint32 uint64
// プラットフォーム:int/uint は 64 ビット
// サイズ:
// byte 1 バイト rune 4 バイト

数値演算

算術演算、ビット演算、math パッケージを使用できます。整数の除算は切り捨てで、オーバーフローに注意してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a, b := 7, 2
a / b // 3(整数除算は切り捨て)
a % b // 1
// 浮動小数点:
// math.Sqrt(16) math.Pow(2, 3)
// math.Max/Min math.Abs
// ビット演算:
// & | ^ << >>
// オーバーフロー:
// var x int8 = 127; x++ // -128
// オーバーフロー検出(1.18+):
// a, ok := bits.Add(a, b, 0)
// 乱数:math/rand
// 定数演算は任意精度

struct

struct はフィールドを組み合わせた型です。フィールド名または位置で初期化でき、フィールドタグでメタデータを付与できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type User struct {
Name string
Age int
Tags []string
}
// 初期化:
u := User{Name: "Nick", Age: 30}
u2 := User{"Anna", 25, nil} // 位置指定(非推奨)
// アクセス:
u.Name = "New"
// ゼロ値:
var u3 User // すべてゼロ値
// フィールドタグ:
// type S struct {
// ID int `json:"id"`
// }
// ポインタは自動でデリファレンス:
p := &u
p.Name = "X" // (*p).Name

配列

配列は固定長で値型(コピー)です。日常的にはあまり使わず、ほとんどの場合はスライスを使用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr [3]int // [0 0 0]
arr[0] = 10
// 初期化:
arr2 := [3]int{1, 2, 3}
arr3 := [...]int{1, 2, 3} // 長さを推論
// 長さ:len(arr)
// 値型:
b := arr2
b[0] = 99 // arr2 は変わらない
// 比較:
// [3]int{1,2,3} == [3]int{1,2,3}
// 配列を引数にするとコピーされる
// 参照はスライスかポインタで渡す

type 定義

type で新しい型やエイリアスを定義します。定義した型は元と異なるため、変換が必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Celsius float64
type ID string
// 新しい型:
var c Celsius = 25.0
// 異なるメソッド集合、変換が必要:
var f float64 = float64(c)
// メソッド:
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", c)
}
// 型エイリアス(1.9+):
type MyInt = int // 完全に同一
// 名前付き型を定義するとメソッドを追加できる
// スライス/map ベースの定義:
type Stack []int

ジェネリクス

ジェネリック関数は [T any] のような型パラメータを使用します。制約インターフェースで受け入れる型を限定できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
// ジェネリック関数:
func Min[T int | float64](a, b T) T {
if a < b { return a }
return b
}
// 制約:
// any は任意の型を表す
// comparable は比較可能
// ジェネリック型:
type Pair[K comparable, V any] struct {
Key K
Value V
}
// 使用:
p := Pair[string, int]{Key: "a", Value: 1}
fmt.Println(Min(1, 2), Min(1.5, 2.5))
// 制約インターフェース:
// type Number interface { ~int | ~float64 }

interface 型

インターフェースはメソッドの集合を定義します。空インターフェース interface{}(any)は任意の型を表します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Shape interface {
Area() float64
}
// 実装:
type Circle struct{ R float64 }
func (c Circle) Area() float64 {
return 3.14 * c.R * c.R
}
// 使用:
func PrintArea(s Shape) {
fmt.Println(s.Area())
}
// 空インターフェース(1.18+ は any):
func PrintAny(v any) {
fmt.Println(v)
}
// 型アサーション:
if c, ok := s.(Circle); ok {
fmt.Println(c.R)
}
// インターフェース値 = 動的型 + 値

comparable 制約

comparable は組み込みの制約で、== で比較できる型を表します。マップのキーやジェネリックの比較に使用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// comparable 制約:
func Contains[T comparable](items []T, target T) bool {
for _, v := range items {
if v == target {
return true
}
}
return false
}
// 比較可能な型:
// 数値、文字列、bool、ポインタ、
// struct(フィールドがすべて比較可能)、配列
// 比較不可:
// slice、map、func、それらを含む struct
// 使用:
// Contains([]string{"a"}, "a")
// map のキーは比較可能でなければならない
// ジェネリック制約は comparable で == エラーを代替

4.ポインタ

ポインタとアドレス操作:アドレス取得、ポインタ間接参照、new/make、スタックとヒープについて説明します。

アドレス取得 &

& は変数のアドレスを取得してポインタを返します。ポインタは変数のメモリ位置を保持します。

1
2
3
4
5
6
7
8
9
10
11
12
13
n := 42
p := &n // p は n を指すポインタ
fmt.Println(p) // アドレス
fmt.Println(*p) // 42
// ポインタ型:*int
// 指した変数は変更できる:
*p = 99
fmt.Println(n) // 99
// & はフィールド/配列要素に:
// &arr[0]
// ポインタはリテラルのアドレスを取れない:
// &42 エラー
// 構造体のアドレス:&User{...}

デリファレンス *

*p でポインタが指す値にアクセスします。構造体のポインタは自動的に間接参照されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Point struct{ X, Y int }
pt := Point{1, 2}
p := &pt
// 自動デリファレンス:
p.X = 10 // (*p).X
(*p).Y = 20
// 明示的なデリファレンスが必要な場面:
q := new(int)
*q = 7
// チェーン:
// (*ptr).field.method()
// 空ポインタのデリファレンスは panic:
// var p *int; *p = 1 // クラッシュ
// 複数レベルのポインタ * *int

nil ポインタ

nil ポインタはゼロ値です。nil をデリファレンスするとパニックしますので、使用前にチェックしてください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var p *int // nil
// 判定:
if p != nil {
fmt.Println(*p)
} else {
fmt.Println("p 是 nil")
}
// nil のデリファレンスはクラッシュ:
// *p = 1 // panic: nil pointer
// よくあるパターン:
func safeDeref(p *int) int {
if p == nil {
return 0
}
return *p
}
// nil を返して無値を表す:
// 関数は (T, bool) か *T を返す

ポインタ引数

ポインタ引数を使うと呼び出し元の変数を変更できます。値引数はコピーです。大きな構造体はポインタで渡すとコピーを節約できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 値引数(コピー):
func setZeroValue(n int) {
n = 0 // コピーのみ変更
}
// ポインタ引数(呼び出し側を変更):
func setZeroPtr(n *int) {
*n = 0
}
// 使用:
x := 100
setZeroPtr(&x)
fmt.Println(x) // 0
// 構造体ポインタ:
func Scale(p *Point, f int) {
p.X *= f
p.Y *= f
}
// 大きい構造体はポインタで渡してコピーを省く
// 変更を永続化したいならポインタ

new と make

new は値型にメモリを確保してポインタを返し、make はスライス・マップ・チャネルを初期化します。用途が異なります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// new:ポインタとゼロ値を返す
p := new(int)
// *p == 0
pp := new(User) // ゼロ値の構造体ポインタ
// make:参照型を初期化
nums := make([]int, 0, 10) // slice
m := make(map[string]int) // map
ch := make(chan int) // channel
// 違い:
// new(T) は *T とゼロ値を返す
// make は slice/map/channel のみ
// 初期化済みの非 nil 値を返す
// map は make しないと書き込めない
// slice は nil 宣言でも append できる

ポインタのイディオム

ポインタは可変状態を共有するために使います。ポインタと値のどちらを返すかはセマンティクスとメソッドレシーバで判断します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ポインタを返す:
func NewUser(name string) *User {
return &User{Name: name}
}
// ポインタレシーバのメソッド:
func (u *User) SetName(name string) {
u.Name = name // 元のオブジェクトを変更
}
// 値レシーバ:
func (u User) FullName() string {
return u.Name
}
// ポインタを使うとき:
// 1. レシーバを変更する
// 2. 大きい構造体のコピーを避ける
// 3. 意味的に共有したい
// 値を使うとき:
// 小さい構造体、不変な意味
// ポインタの一貫性:同じ型のメソッドは統一

スタックとヒープ

変数がスタックとヒープのどちらに置かれるかはコンパイラがエスケープ解析で決定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ローカル変数は既定でスタック:
func local() int {
x := 42
return x // 戻り値、x はスタック割り当て可
}
// アドレスを取るとヒープに逃げる:
func escape() *int {
x := 42
return &x // x はヒープへ逃げる
}
// エスケープの確認:
// go build -gcflags='-m' .
// エスケープの原因:
// 1. ローカル変数のアドレスを返す
// 2. アドレスをグローバル/ヒープオブジェクトに格納
// 3. インターフェースのボックス化、クロージャの捕捉
// エスケープは GC 負荷を増やすが、過度な最適化は不要

ポインタ安全性

Go のポインタは算術演算をしません(C 風の +1 は不可)。GC がライフサイクルを管理するためダングリングポインタは発生しません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ポインタ演算はない:
// p = p + 1 エラー
// 存在しない操作
// 安全なアクセス(コンパイラが保証):
// 範囲外/ダングリングは起きない
// ポインタとスライス:
nums := []int{1, 2, 3}
p := &nums[0]
// ポインタは配列要素を指す
// unsafe パッケージで回避(慎重に):
// import "unsafe"
// 変換は手動で安全を保証する必要がある
// 空ポインタ検査:デリファレンス前に nil 判定
// メモリ安全性は GC + コンパイル時に保証

5.制御フロー

if、for、switch、defer、ループ制御について説明します。

if / else

if の条件に括弧は不要です。条件の前に初期化文を置けます。条件はブール値でなければなりません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// 初期化文:
if err := doSomething(); err != nil {
fmt.Println(err)
}
// err は if/else ブロック内でのみ可視
// 条件はブールでなければならない(非ゼロ判定は不可)
// 代入は比較ではない:
// if ok = true エラー、if ok が必要

for ループ

Go には for ループしかありません。C 風の 3 段形式、while 風の条件式のみ、無限ループの 3 つの形式があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 完全な形式:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while 形式:
n := 0
for n < 5 {
n++
}
// 無限ループ:
for {
break // または return
}
// 複数変数:
for i, j := 0, 10; i < j; i, j = i+1, j-1 {
}
// 初期化/条件の省略:
// for ; n < 3; n++

range 反復

range は配列、スライス、マップ、文字列、チャネルを反復します。Go 1.22 以降、ループ変数は反復ごとに独立します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
nums := []int{1, 2, 3}
for i, v := range nums {
fmt.Println(i, v)
}
// 値だけ:
for _, v := range nums
// インデックスだけ:
for i := range nums
// map の走査:
for k, v := range m { }
// 文字列の走査(rune ごと):
for i, r := range "你好" {
fmt.Println(i, r) // rune ごと
}
// channel の走査:
for v := range ch { }
// 1.22+ はループ変数が毎回独立したコピー
// 古いバージョンの goroutine クロージャは要注意

switch

switch は case を自動で降りません。複数の値、無式(if-else の連鎖)、型スイッチが使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
day := "mon"
switch day {
case "mon", "tue":
fmt.Println("工作日")
case "sat", "sun":
fmt.Println("周末")
default:
fmt.Println("其他")
}
// 式なし(ブール条件):
score := 75
switch {
case score >= 90:
fmt.Println("A")
default:
fmt.Println("其他")
}
// 型の判定:
var v any = "str"
switch t := v.(type) {
case string:
fmt.Println("string", t)
case int:
fmt.Println("int", t)
}

defer

defer は関数の終了まで実行を遅らせ、LIFO 順で実行されます。リソース解放によく使われます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func readFile() error {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // 関数の終了時に閉じる
// 処理...
return nil
}
// 複数の defer は後入れ先出し:
func example() {
defer fmt.Println("first")
defer fmt.Println("second") // 先に出力される
}
// defer の引数は宣言時に評価:
func count() {
i := 0
defer fmt.Println(i) // 0
i = 10
}
// 用途:
// ファイル/接続のクローズ、ロック解除、panic からの回復

break と continue

break はループを抜け、continue は次の反復へ進みます。ラベルで入れ子のループも制御できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
if i > 7 {
break
}
fmt.Println(i) // 1 3 5 7
}
// ラベルで外側を制御:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // 外側の次の反復へ
}
fmt.Println(i, j)
}
}
// break outer で外側のループを終了
// ラベルはループと同じ行か前の行に置く

初期化付き switch

switch の前に初期化文を書けます。エラーハンドリングのチェインなどで便利です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch n := rand.IntN(3); n {
case 0:
fmt.Println("零")
case 1:
fmt.Println("一")
default:
fmt.Println("其他")
}
// 初期化変数は switch 内でのみ可視
// 等価な書き方:
// n := rand.IntN(3)
// switch n { ... }
// 用途:エラー処理チェーン:
// switch err := f(); err {
// case nil: ...
// default: log(err)
// }
// case が一致しなければ自動で default へ

goto とラベル

goto はラベルへジャンプします。Go ではサポートされていますが、あまり使われません。深いネストのエラー処理などに限定されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func process(items []int) error {
for i, v := range items {
if v < 0 {
goto failure
}
_ = i
}
return nil
failure:
return errors.New("发现负数")
}
// 注意点:
// ラベルは使用後(後ろ)に宣言する必要がある
// 変数宣言の中へ/を飛び越えることはできない
// goto は別のブロックへ飛べない
// 多くの場面で defer/エラーリターンの方が明確
// 慎重に使う、可読性を優先

6.関数

関数定義、複数の戻り値、クロージャ、メソッド、関数値を扱います。

関数定義

func で関数を定義します。引数は型を伴い、戻り型は最後に書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
// 引数型の省略:
func sub(a, b int) int {
return a - b
}
// 戻り値なし:
func log(msg string) {
fmt.Println(msg)
}
// 呼び出し:
func main() {
sum := add(1, 2)
fmt.Println(sum)
}
// 関数の引数は値渡し(コピー)
// 参照はポインタ/slice/map で渡す

複数の戻り値

Go の関数は複数の値を返せます。慣習として (値, error) を返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数为零")
}
return a / b, nil
}
// 使用:
result, err := div(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println(result)
}
// 一部を無視:
q, _ := div(10, 2)
// 複数の有効値:
func minMax(nums []int) (int, int) {
return nums[0], nums[len(nums)-1]
}
// 慣例:
// 最後の戻り値は error
// エラー時は他の戻り値をゼロ値に

名前付き戻り値

戻り値に名前を付けられます。関数内で代入し、裸の return で自動的に返すことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 裸の return で x, y
}
// 使用場面:
func stats(nums []int) (sum, avg float64) {
sum = 0
for _, v := range nums {
sum += float64(v)
}
avg = sum / float64(len(nums))
return
}
// 注意:
// 裸の return は可読性に議論がある
// defer は名前付き戻り値を変更できる
// 名前付き戻り値はゼロ値を持つ
// 複雑な戻り値は名前付きが明確

可変長引数

...T で任意の数の引数をスライスとして受け取れます。呼び出し側は slice... で展開できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 呼び出し:
sum(1, 2, 3) // 6
sum() // 0
// スライスの展開:
nums := []int{1, 2}
sum(nums...) // 複数の引数に展開
// 混合:
// func greet(prefix string, names ...string)
// 可変引数は最後でなければならない
// 型は []T(スライスと同じ)

クロージャ

クロージャは外側の変数をキャプチャします。返された関数は独自の状態を保ちます。1.22 以降、ループ変数は反復ごとにキャプチャされます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func makeCounter() func() int {
count := 0
return func() int {
count++ // count を捕捉
return count
}
}
// 使用:
counter := makeCounter()
counter() // 1
counter() // 2
// クロージャが捕捉するのは変数自体:
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
// それぞれ独立した状態:
c1, c2 := makeCounter(), makeCounter()
// 1.22+ はループ変数が毎回独立

メソッド

メソッドは型に紐づき、レシーバは func とメソッド名の間に書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Rectangle struct {
Width, Height float64
}
// 値レシーバ:
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// ポインタレシーバ:
func (r *Rectangle) Scale(f float64) {
r.Width *= f
r.Height *= f
}
// 使用:
rect := Rectangle{2, 3}
fmt.Println(rect.Area())
rect.Scale(2)
// メソッドと関数の違い:
// メソッドは型に紐づく
// メソッドは自分のパッケージの型にのみ定義できる
// ポインタレシーバは元のオブジェクトを変更できる

関数値

関数は第一級市民で、変数代入・引数・戻り値に使えます。関数型はシグネチャで表します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 関数型:
type Operation func(a, b int) int
func add(a, b int) int { return a + b }
func apply(op Operation, x, y int) int {
return op(x, y)
}
// 使用:
result := apply(add, 3, 4) // 7
// 匿名関数:
f := func(a, b int) int { return a - b }
// 引数として(コールバック):
func filter(nums []int, pred func(int) bool) []int {
out := make([]int, 0)
for _, n := range nums {
if pred(n) {
out = append(out, n)
}
}
return out
}
// よく使う:sort.Slice に比較関数を渡す

init 関数

init はパッケージがインポートされたときに自動で実行されます。1 つのファイルに複数の init を書けます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
var config map[string]string
func init() {
config = make(map[string]string)
config["env"] = "dev"
fmt.Println("初始化完成")
}
// 特徴:
// main の前に実行される
// 各ファイルに複数の init を書ける
// 複数の init はファイル/順序に従って実行
// 依存パッケージの init が先に実行
// 用途:
// ドライバの登録、設定の初期化
// var で初期化できない場合
// init の順序に依存するのは避ける

7.文字列

不変文字列、rune/バイト、strings パッケージ、フォーマットについて説明します。

文字列の基礎

文字列は不変のバイト列です。ダブルクォートは通常の文字列、バッククォートは生の文字列リテラルです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s := "hello"
// バッククォートの生文字列:
raw := `多行
文本`
// 長さ(バイト数):
len("hello") // 5
len("你好") // 6(UTF-8 は 1 文字 3 バイト)
// インデックス(バイト):
s[0] // 'h' (byte)
// 連結:
s2 := "a" + "b"
// 不変:
// s[0] = 'x' エラー
// コピー:
// 文字列の代入は参照のコピー(不変で安全)
// 文字列は比較可能:== < >

rune と文字

rune は Unicode コードポイント(int32)です。range は rune ごとに進み、unicode/utf8 でデコードします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "你好, Go"
// range は rune ごと:
for _, r := range s {
fmt.Printf("%c ", r)
}
// 你好 , 空格 G o
// rune スライスへ変換:
rs := []rune(s)
fmt.Println(len(rs)) // rune の数
// バイトスライス:
bs := []byte(s)
// rune リテラル:
ch := '中' // rune
// デコード:
import "unicode/utf8"
// utf8.RuneCountInString(s)
// utf8.DecodeRuneInString
// インデックスはバイト位置、len で中国語を切らない

strings.Builder

Builder は文字列を効率的に連結します。ループ内での + 連結より割り当てを抑えられます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "strings"
var b strings.Builder
b.WriteString("hello")
b.WriteString(", ")
b.WriteString("world")
result := b.String() // "hello, world"
// 事前割り当て:
b.Grow(100) // 事前の容量
// バイト/文字を書く:
b.WriteByte('!')
b.WriteRune('中')
// 長さを返す:
b.Len()
// 用途:
// ループでの連結、HTML/CSV 生成
// + 連結より効率的(複数回の割り当てを回避)
// bytes.Buffer より文字列の意味に合致

strings パッケージ

Contains、Split、Join、Trim、Replace などさまざまな文字列操作があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "strings"
s := " Hello World "
strings.TrimSpace(s)
strings.ToUpper(s)
strings.ToLower(s)
strings.Contains(s, "World") // true
strings.HasPrefix(s, " ")
strings.HasSuffix(s, " ")
strings.Replace(s, "World", "Go", -1)
strings.ReplaceAll(s, " ", "-")
// 分割:
parts := strings.Split("a,b,c", ",")
strings.Join([]string{"a", "b"}, "-")
// 部分文字列:
strings.Index(s, "World")
strings.LastIndex(s, "o")
// 数える:
strings.Count(s, "o")
// 比較:
strings.Compare("a", "b")
strings.EqualFold("Go", "go") // 大文字小文字を無視

fmt フォーマット

Printf/Sprintf の動詞、幅、精度、パディングを制御できます。Fprintf は io.Writer に書き出します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name, age := "Nick", 30
fmt.Printf("%s 今年 %d 岁\n", name, age)
// 動詞:
// %v デフォルト %+v フィールド名付き %#v Go 構文
// %d 十進数 %x 十六進数
// %f 浮動小数点 %.2f 2 桁
// %s 文字列 %q 引用符付き
// %t ブール %T 型
// %5d 幅 %-5d 左寄せ
// %05d ゼロ埋め
// %10.2f 幅 10 2 桁
// 文字列を返す:
s := fmt.Sprintf("%s-%d", name, age)
// io.Writer に書く:
fmt.Fprintf(buf, "%s", name)

strconv 変換

文字列と数値、ブール値の相互変換を行います。クォートやエスケープも扱えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "strconv"
// 数値を文字列に:
s := strconv.Itoa(42) // "42"
// 文字列を数値に:
n, err := strconv.Atoi("42")
// 浮動小数点を解析:
f, err := strconv.ParseFloat("3.14", 64)
// 基数を指定:
n64, _ := strconv.ParseInt("101", 2, 64)
// フォーマット:
strconv.FormatInt(255, 16) // "ff"
strconv.FormatFloat(3.14, 'f', 2, 64)
// ブール:
b, _ := strconv.ParseBool("true")
strconv.FormatBool(true)
// 引用符:
strconv.Quote("hi") // "\"hi\""
// 基数フォーマット:
fmt.Sprintf("%x", 255) // "ff"

エンコード処理

base64、hex、URL エンコードを扱います。[]byte と文字列の相互変換もよく使われます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"encoding/base64"
"encoding/hex"
"net/url"
)
// base64:
b64 := base64.StdEncoding.EncodeToString([]byte("hi"))
decoded, _ := base64.StdEncoding.DecodeString(b64)
// hex:
h := hex.EncodeToString([]byte("\x00\xff"))
// URL エンコード:
url.QueryEscape("a b&c") // "a+b%26c"
// バイトと文字列:
s := string([]byte{104, 105})
bs := []byte(s)
// 十六進数出力:
fmt.Printf("%x\n", "hi")
// エンコード/デコードのエラーは err を処理

文字の反復

非 ASCII のテキストを安全に処理するには rune 単位で反復します。インデックスはバイト単位である点に注意してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "你好 Go"
// range で rune ごとに走査:
for _, r := range s {
fmt.Printf("%c", r)
}
// インデックスが必要な場合:
for i, r := range s {
fmt.Println(i, r) // バイトオフセット + コードポイント
}
// バイトインデックス(純 ASCII):
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
// []rune に変換:
rs := []rune(s)
// n 番目の文字を取得:
fmt.Println(string(rs[2]))
// 中国語は必ず rune 単位で処理

8.コレクション

スライス、マップ、配列、ソートについて説明します。

スライス slice

スライスは基底配列に対する動的なビューです。nil スライスも長さ 0 で append などが可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nums := []int{1, 2, 3} // リテラル
var s []int // nil slice
// 追加:
nums = append(nums, 4)
// 空の slice:
empty := []int{}
// 長さと容量:
len(nums); cap(nums)
// スライス:
sub := nums[1:3] // ビューが基底配列を共有
// 基底配列を参照:
// sub を変更すると nums に影響
// append が容量を超えると新配列を割り当て
// make で容量を指定:
make([]int, 5, 10)

スライス操作

append で追加、copy で複製、append のイディオムで削除・挿入を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nums := []int{1, 2, 3, 4, 5}
// 複数追加:
nums = append(nums, 6, 7)
// スライスの連結:
nums = append(nums, []int{8, 9}...)
// インデックス 2 を削除:
nums = append(nums[:2], nums[3:]...)
// コピー:
dst := make([]int, len(nums))
copy(dst, nums)
// 先頭に挿入:
nums = append([]int{0}, nums...)
// クリア:
nums = nums[:0]
// 反転:
for i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 {
nums[i], nums[j] = nums[j], nums[i]
}
// コピーは基底配列共有に注意

make と容量

make はスライスを生成してキャパシティを事前確保します。append による再アロケーションを防ぎ、cap で成長戦略を制御します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 長さ 0 容量 5:
nums := make([]int, 0, 5)
// 事前割り当てで複数回のコピーを回避:
nums = append(nums, 1, 2, 3)
// 容量の増加:
// append が容量を超えると倍に拡張
// 大きいスライスは事前割り当て:
// make([]int, 0, 100000)
// 長さと容量:
s := make([]int, 5) // 長さ 5、ゼロ値
cap(s) // 5
// クリアして容量を保持:
s = s[:0]
// 空の判定:
if len(s) == 0
// cap で事前割り当てし性能を最適化

map

マップはキーと値の対を保持します。make で作成し、読み書き・削除・反復が可能です。nil マップは読み取り専用です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
// 読み取り:
v := m["a"] // 存在しないとゼロ値を返す
v, ok := m["a"] // ok で存在を判定
if val, ok := m["x"]; ok {
fmt.Println(val)
}
// 削除:
delete(m, "a")
// 走査(順序なし):
for k, v := range m {
fmt.Println(k, v)
}
// 長さ:len(m)
// リテラル:
mm := map[string]int{"a": 1}
// nil map:
// var mm map[string]int
// mm["a"] = 1 // panic

map のイディオム

マップはセット、カウンタ、キャッシュとして利用できます。キーによる値の集約にも便利です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 集合(set):
seen := make(map[string]bool)
for _, v := range list {
if !seen[v] {
seen[v] = true
fmt.Println("新:", v)
}
}
// カウント:
counts := make(map[string]int)
for _, w := range words {
counts[w]++
}
// キャッシュ:
var cache = make(map[string][]byte)
func get(key string) ([]byte, bool) {
data, ok := cache[key]
return data, ok
}
// 構造体の値:
m := map[string]Point{}
// ネストの更新:
// 値コピーの罠:p := m["a"]; p.X=1; m["a"]=p

ソート

sort パッケージでスライスをソートできます。比較関数を渡したり sort.Search で二分探索をしたりできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "sort"
// 基本ソート:
nums := []int{3, 1, 2}
sort.Ints(nums)
strs := []string{"b", "a"}
sort.Strings(strs)
// カスタム比較:
type Person struct{ Name string; Age int }
people := []Person{{Age: 30}, {Age: 20}}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// 逆順:
sort.Slice(people, func(i, j int) bool {
return people[i].Age > people[j].Age
})
// 安定ソート:
sort.SliceStable(people, ...)
// 二分探索:
// sort.SearchInts(nums, 2)
// 整列済み判定:sort.IntsAreSorted(nums)

container パッケージ

container/heap でヒープ、container/list で双方向連結リストを使えます。データ構造が必要なときに利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// container/list(連結リスト):
import "container/list"
l := list.New()
l.PushBack(1)
l.PushFront(0)
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
// ヒープ:
import "container/heap"
// heap.Interface を実装:
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h; n := len(old); x := old[n-1]
*h = old[:n-1]; return x
}
// 使用:
h := &IntHeap{3, 1, 2}
heap.Init(h)
heap.Pop(h) // 最小 1

反復のコツ

range でインデックスや値を取り出し、マップではキーをソートして走ると順序が安定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// インデックス走査:
for i := range nums {
nums[i] *= 2
}
// インデックス + 値:
for i, v := range nums {
fmt.Println(i, v)
}
// 値のみ:
for _, v := range nums
// 文字列 rune:
for i, r := range s
// map キーをソートして走査:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// スライスを逆順で走査:
for i := len(nums) - 1; i >= 0; i--

9.メモリと性能

ガベージコレクション、エスケープ解析、割り当て最適化、メモリプロファイリングを扱います。

ガベージコレクション

Go は並行・低レイテンシの自動 GC を備えています。free を呼ぶ必要はありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Go は並行マークスイープ GC を使用
// 手動の malloc/free は不要
// 割り当て:
// ヒープオブジェクトは GC が回収
// スタックオブジェクトは関数が戻れば即解放
// GC パラメータ:
// GOGC=100 デフォルト閾値
// debug.SetGCPercent()
// トリガー:
// メモリ 2 倍/明示的 runtime.GC()
// 影響:
// ヒープ割り当てを減らすと GC 負荷が下がる
// 大量の小オブジェクトは sync.Pool を使う
// 大きいオブジェクトはライフサイクルに注意

エスケープ解析

コンパイラが変数の配置場所を決定します。アドレス取得やインターフェースへの格納でエスケープが発生することがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// スタック割り当て:
func local() int {
x := 42 // エスケープしない、スタック上
return x
}
// ヒープ割り当て(エスケープ):
func leak() *int {
x := 42
return &x // アドレスが返され、エスケープ
}
// エスケープの確認:
// go build -gcflags='-m' ./...
// エスケープのトリガー:
// 1. ローカル変数のアドレスを返す
// 2. クロージャが変数を捕捉
// 3. インターフェースに値を格納(ボックス化)
// 4. グローバル変数への代入
// 最適化:
// 不要なエスケープを避け GC 負荷を減らす
// ただし過度な最適化は不要

割り当て最適化

キャパシティの事前確保、バッファの再利用、小さな割り当ての削減でホットパスを最適化します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// スライスを事前割り当て:
nums := make([]int, 0, n) // 繰り返しの拡張を回避
// バッファの再利用:
var buf bytes.Buffer
buf.Grow(1024) // 事前割り当て
// 一時オブジェクトを避ける:
// ループ内での fmt.Sprintf 生成を避ける
// builder で連結:
var sb strings.Builder
// オブジェクトプール:
// 短命なオブジェクトを再利用
// インターフェース呼び出しのオーバーヘッド:
// ホットパスでのインターフェース経由を避ける
// 分析が最適化に先立つ:
// pprof でホットスポットを見つける
// 普通のコードを早すぎる最適化しない

ファイナライザ

runtime.SetFinalizer でオブジェクト回収前にクリーンアップを登録できます。外部リソースの安全網として有用です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "runtime"
type Resource struct {
name string
handle *os.File
}
func NewResource(name string) *Resource {
r := &Resource{name: name}
runtime.SetFinalizer(r, func(r *Resource) {
if r.handle != nil {
r.handle.Close() // 保険のクリーンアップ
}
})
return r
}
// 注意点:
// ファイナライザは即時実行を保証しない
// プログラム終了時に未処理のファイナライザは実行されない
// ベストプラクティス:
// 明示的な Close を主に
// ファイナライザは保険にのみ
// 循環参照はファイナライザが永遠に走らない原因に

メモリモデル

並行下では、書き込みは同期イベント(チャネル、ミューテックス、atomic)を介してのみ他の goroutine から見えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Go メモリモデル:
// ある goroutine の書き込みが
// 別の goroutine の読み取り前に見えるようにするには、
// 同期プリミティブが必要:
// 1. channel:
ch := make(chan int)
gr() { ch <- 1 }() // 書き込み
<-ch // 読み取り
// 2. ロック:
var mu sync.Mutex
mu.Lock()
// 臨界領域
mu.Unlock()
// 3. atomic:
var x atomic.Int64
x.Store(1)
v := x.Load()
// 同期なしの共有読み書きはデータレース
// go run -race で検出

メモリプロファイリング

pprof でメモリと CPU をプロファイリングできます。ヒープスナップショットはリーク調査に有効です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
_ "net/http/pprof"
"net/http"
)
// pprof を起動:
// go run -race main.go
// コード内で:
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ...
}
// 分析:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/profile
// コマンドライン操作:
// top / list / web
// フレームグラフ:
// go tool pprof -http=:8080 profile.out
// 特定:割り当てホットスポット、リークオブジェクト

sync.Pool

Pool は短命なオブジェクトを再利用して GC 圧を下げます。バッファやデコーダーに向いています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// 使用:
b := bufPool.Get().(*bytes.Buffer)
b.Reset() // 再利用前にクリア
// 書き込みに使用...
bufPool.Put(b) // 返却
// 適用:
// 大量の短命オブジェクト
// 一時バッファ、シリアライズバッファ
// 注意:
// GC は Pool を空にする(保持は保証されない)
// 不適用:長期間保持するオブジェクト
// Get は nil を返す可能性がある(New 未設定)
// 状態を持つオブジェクトを Pool に入れない

スタックと再帰

goroutine のスタックは動的に拡張しますが、再帰が深すぎると制限に達します。深い再帰はループに書き換えましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// goroutine スタック:
// 初期 2KB、動的に拡張
// スタック上限は約 1GB(64 ビット)
// 再帰:
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
// 深い再帰のリスク:
// スタックオーバーフローでクラッシュ:fatal error: stack overflow
// 深い再帰は反復に:
func factorialIter(n int) int {
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result
}
// 再帰深度のメモ:
// 数万層はスタック増大に注意

10.構造体とメソッド

Go の OOP:構造体、インターフェース、埋め込みと合成(クラス継承はありません)。

struct とメソッド

構造体はデータを保持し、メソッドで振る舞いを結び付けます。コンストラクタ関数で生成するのが慣習です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Account struct {
ID string
Balance float64
}
// 値レシーバ:
func (a Account) Balance() float64 {
return a.Balance
}
// ポインタレシーバ:
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
// コンストラクタ:
func NewAccount(id string) *Account {
return &Account{ID: id}
}
// 使用:
acc := NewAccount("a1")
acc.Deposit(100)
// 値/ポインタの自動変換:
// acc ポインタは値メソッドを呼べる

レシーバの選択

値レシーバは元の値を変更せず、ポインタレシーバは変更します。同じ型のメソッドはレシーバの種類を統一しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Counter struct {
N int
}
// 値レシーバ(コピー):
func (c Counter) Value() int {
return c.N
}
// ポインタレシーバ(変更):
func (c *Counter) Inc() {
c.N++
}
// 使用:
c := Counter{}
c.Inc() // 自動でアドレス取得
// 規則:
// レシーバを変更する -> ポインタ
// 大きい構造体 -> ポインタ(コピー回避)
// すべてのメソッドのレシーバを統一:
// 混在すると自動アドレス取得が生じる
// インターフェース実装もレシーバ一致が必要

インターフェース

インターフェースは振る舞いの契約を表します。メソッド集合が一致すれば暗黙的に実装になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Greeter interface {
Greet() string
}
// 型の実装:
type Dog struct{ Name string }
func (d Dog) Greet() string {
return "汪汪 " + d.Name
}
type Cat struct{ Name string }
func (c Cat) Greet() string {
return "喵 " + c.Name
}
// 使用:
func GreetAll(gs []Greeter) {
for _, g := range gs {
fmt.Println(g.Greet())
}
}
// 実装は暗黙的:
// メソッド集合が一致すれば実装したことになる
// implements 宣言は不要
// インターフェース値は具象型を含む
// インターフェースを引数にすると疎結合

インターフェースと nil

nil インターフェースと、型付きの nil を保持するインターフェースは異なります。判別に注意しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Error interface {
Error() string
}
// 落とし穴:
func returnsNil() *MyError {
return nil
}
var err error = returnsNil()
// err != nil だが内部は nil ポインタ
if err != nil {
fmt.Println("有错误") // 誤判定
}
// 原因:インターフェース = 型 + 値
// nil ポインタにも型情報がある
// 解決:
// 1. *T ではなく error を返す
// 2. 空判定はリフレクション:
// reflect.ValueOf(err).IsNil()
// 3. インターフェース内に nil ポインタを入れない規約
// error を返すとき nil は error の nil であることを保証

埋め込み

埋め込みフィールドやインターフェースにより、そのメソッドが昇格されます。継承より合成を選びましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Base struct {
ID int
}
func (b Base) GetID() int { return b.ID }
type User struct {
Base // 埋め込み
Name string
}
// 昇格:
u := User{Base: Base{ID: 1}, Name: "Nick"}
u.GetID() // 昇格したメソッド
u.ID // 昇格したフィールド
// オーバーライド:
type Admin struct {
Base
Permissions []string
}
func (a Admin) GetID() int {
return a.Base.ID * 100 // カスタム
}
// interface の埋め込みも可:
// type Reader interface { io.Reader }
// 継承の代わりに組み合わせ:
// フィールド + メソッドの昇格

合成

Go は継承ではなく合成で再利用を実現します。小さなインターフェースを組み合わせて大きな機能を作ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// インターフェースの組み合わせ:
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type ReadWriter interface {
Reader
Writer
}
// 構造体の組み合わせ:
type File struct {
name string
}
type GzipFile struct {
File // ファイル処理を再利用
level int
}
// 組み合わせは再利用を提供し、継承の脆さがない
// 小さなインターフェースの原則:
// インターフェースは小さいほど満たしやすい
// 必要なときにだけ組み合わせる
// 構造の入れ子で機能を再利用

型アサーション

インターフェースを具体的な型にアサートします。ok 形式で安全に扱い、type switch で分岐できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var v any = "hello"
// 安全な型アサーション:
s, ok := v.(string)
if ok {
fmt.Println(s)
}
// 不安全な型アサーション(失敗は panic):
// s := v.(string)
// type switch:
switch val := v.(type) {
case string:
fmt.Println("string", val)
case int:
fmt.Println("int", val)
default:
fmt.Println("未知", val)
}
// アサーション失敗はゼロ値 + false を返す
// より具体的なインターフェースへの断言:
// if r, ok := w.(io.Reader); ok

any / 空インターフェース

any は interface{} のエイリアスで任意の型を保持します。使うときは型アサーションが必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// any は interface{} の別名:
var v any
v = 42
v = "str"
// 取り出すには型アサーションが必要:
s := v.(string)
// ジェネリックで代替:
// 型パラメータが必要ならジェネリック:
func Print[T any](v T) {
fmt.Println(v)
}
// 空インターフェースをコンテナに:
var store = make(map[string]any)
store["age"] = 30
store["name"] = "Nick"
// 取り出すときに型アサーション:
age, ok := store["age"].(int)
// 慎重に:
// 型安全性を失う
// 明示的な型/ジェネリックを優先
// シリアライズ時、JSON では any が一般的

11.エラー処理

error インターフェース、エラーラップ、errors.Is/As、panic/recover を扱います。

error インターフェース

error は組み込みインターフェースで、Error() string メソッドを持ちます。例外ではなくエラー値を返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type error interface {
Error() string
}
// エラーを返す:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数为零")
}
return a / b, nil
}
// 判定:
result, err := divide(10, 0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
// カスタムエラー型:
type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return "字段 " + e.Field + " 无效"
}
// nil エラーは成功を表す

エラーの返し方

エラーは呼び出しチェーンを遡って返します。各段で err != nil をチェックして処理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func openConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // そのまま伝播
}
if len(data) == 0 {
return nil, errors.New("配置为空")
}
return data, nil
}
// 呼び出しチェーン:
func load() error {
data, err := openConfig("app.json")
if err != nil {
return err // さらに上位へ伝播
}
_ = data
return nil
}
// 慣例:
// エラー時はゼロ値 + err を返す
// err が nil のとき他方の戻り値は有効
// エラーを握りつぶさない:
// 少なくとも log するか返す

エラーメッセージ

fmt.Errorf でフォーマット付きエラー、errors.New で単純なエラーを生成します。%w でラップできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "fmt"
// シンプルなエラー:
// errors.New("失败")
// パラメータ付き:
func open(name string) error {
return fmt.Errorf("无法打开 %s", name)
}
// 多層のラップ:
// %w で基底エラーをラップ
func load() error {
err := readFile()
if err != nil {
return fmt.Errorf("load 失败: %w", err)
}
return nil
}
// %v はラップしない:
// fmt.Errorf("失败: %v", err)
// 新しいエラーインスタンス:
// errors.New("message")
// カスタム:
// fmt.Errorf("状态码 %d", 404)

errors.Is

errors.Is はエラーチェーンを辿って一致を判定します。err == での比較よりこちらが推奨されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "errors"
var ErrNotFound = errors.New("not found")
func find(id int) error {
return fmt.Errorf("查找 %d: %w", id, ErrNotFound)
}
// 判定(ラップに対応):
err := find(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("未找到")
}
// 複数ターゲット:
// errors.Is(err, ErrNotFound) || errors.Is(err, ErrTimeout)
// 単一ターゲット:
// センチネルエラーを定義 var ErrX = errors.New(...)
// == は %w のラップを通り抜けられない
// 標準ライブラリの使い方:
// errors.Is(err, os.ErrNotExist)

errors.As

errors.As はエラーチェーンから指定した型のエラーを見つけ出して取り出します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "errors"
type ConfigError struct {
Path string
Msg string
}
func (e *ConfigError) Error() string {
return e.Path + ": " + e.Msg
}
// ラップして投げる:
func loadConfig() error {
return fmt.Errorf("加载: %w",
&ConfigError{Path: "app.json", Msg: "语法错误"})
}
// 取り出し:
err := loadConfig()
var configErr *ConfigError
if errors.As(err, &configErr) {
fmt.Println(configErr.Path, configErr.Msg)
}
// Is との違い:
// Is はエラー値を比較(センチネル)
// As はエラー型を取り出す(フィールド含む)
// カスタムエラーはポインタレシーバ
// サードパーティライブラリのエラーによく使う

panic と recover

panic は goroutine を中断し、recover は defer 内でしか使えません。回復不能な状況にのみ使用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// panic:
func divide(a, b int) int {
if b == 0 {
panic("除数为零") // クラッシュ
}
return a / b
}
// recover で捕捉:
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
f()
return nil
}
// 使用:
// err := safeCall(func() { divide(1, 0) })
// 規則:
// panic は defer 内でのみ recover
// プログラムが継続できないエラーにのみ使う
// panic を通常のエラー処理に使わない
// ライブラリコードでは panic を避ける

defer でのエラー処理

名前付き戻り値を使うと、defer 内で戻り値にエラーを代入できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ファイルを閉じる際のエラー検査:
func read() (err error) {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = closeErr // 戻り値を上書き
}
}()
// 処理...
return nil
}
// 接続を閉じる:
// defer conn.Close()
// ロック解除:
// mu.Lock()
// defer mu.Unlock()
// 一時ファイルのクリーンアップ:
// defer os.Remove(tmp)
// defer の引数は即時評価される点に注意

エラーの結合

errors.Join は複数のエラーを 1 つにまとめます。複数の検証やタスクを一括で扱うのに便利です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "errors"
// 複数のエラーを結合:
err1 := errors.New("数据库失败")
err2 := errors.New("缓存失败")
joined := errors.Join(err1, err2)
fmt.Println(joined)
// 2 つのエラーがそれぞれ 1 行ずつ
// いずれかのエラーを判定:
// errors.Is(joined, err1) // true
// すべて nil なら nil を返す:
// errors.Join(nil, nil) // nil
// タスクエラーの収集:
func runAll() error {
var errs []error
if err := step1(); err != nil {
errs = append(errs, err)
}
if err := step2(); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}
// Go 1.20+
// エラーは複数のターゲットを同時に is できる

12.入出力

ファイル入出力、bufio、JSON、io.Reader/Writer について説明します。

ファイルの読み込み

小さなファイルは os.ReadFile で読み込めます。大きなファイルは os.Open + Reader(bufio.Scanner など)でストリーム処理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "os"
// ファイル全体:
data, err := os.ReadFile("data.txt")
if err != nil {
log.Fatal(err)
}
// 1 行ずつ読み取る:
f, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
// 大きなファイルを 1 行ずつ読めばメモリを占有しない
// チャンク読み取り:
// io.Reader + Read で固定バッファを使う

ファイルの書き込み

os.WriteFile で一括書き込み、os.OpenFile で追記、bufio.Writer でバッファリング書き込みを行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "os"
// 一括書き込み:
err := os.WriteFile("out.txt",
[]byte("hello"), 0644)
// 追記:
f, _ := os.OpenFile("log.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
f.WriteString("更多内容\n")
// バッファ付き書き込み:
w := bufio.NewWriter(f)
w.WriteString("text")
w.Flush() // ファイルにフラッシュ
// 権限:
// 0644 所有者は読み書き、他者は読み
// ディレクトリ作成:os.MkdirAll
// エラー処理を無視しない

JSON シリアライズ

json.Marshal で構造体を JSON に変換します。フィールドタグで JSON キーやオプションを制御できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
// 空値を無視:
Email string `json:"email,omitempty"`
// フィールドを無視:
Secret string `json:"-"`
}
// シリアライズ:
u := User{Name: "Nick", Age: 30}
data, err := json.Marshal(u)
// 整形:
pretty, _ := json.MarshalIndent(u, "", " ")
// map に変換:
m := map[string]any{"k": "v"}
b, _ := json.Marshal(m)
// デフォルトのフィールド名:
// エクスポートしたフィールド名の先頭は小文字になる?いいえ、元の名前を維持
// タグで出力フィールド名を制御

JSON デシリアライズ

json.Unmarshal で JSON を構造体やマップにデコードできます。大きなデータは json.NewDecoder でストリーム処理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
// 文字列を解析:
var u User
err := json.Unmarshal([]byte(`{"name":"Nick","age":30}`), &u)
// map に解析:
var m map[string]any
json.Unmarshal(data, &m)
// map 内の数値は float64:
age := m["age"].(float64)
// ストリーム解析(大きな JSON):
dec := json.NewDecoder(bytes.NewReader(data))
dec.Decode(&u)
// 未知フィールドの無視:
// Decoder.DisallowUnknownFields() はその逆
// エラー処理:
// 構文エラーは *json.SyntaxError を返す

io.Reader / Writer

io.Reader/Writer はストリームを抽象化します。io.ReadAll と io.Copy はよく使うヘルパーです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"io"
"strings"
)
// Reader から全部読む:
r := strings.NewReader("hello")
data, _ := io.ReadAll(r)
// ストリームのコピー:
// io.Copy(dst, src)
// ストリームパイプ:
// io.Pipe()
// 組み合わせ:
// bufio.NewReader / bufio.NewWriter
// 有限読み取り:
// io.LimitReader(r, 100)
// 複数リーダー:
// io.MultiReader(a, b)
// 複数ライター:
// io.MultiWriter(a, b)
// 空実装:
// io.Discard は書き込みを破棄

bufio バッファ

bufio はバッファリングでシステムコールを減らします。Scanner で 1 行ずつ反復できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"bufio"
"os"
)
// 読み取り:
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
// 固定バイトを読み取る:
buf := make([]byte, 1024)
reader.Read(buf)
// Scanner で 1 行ずつ:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
// 書き込み:
w := bufio.NewWriter(os.Stdout)
w.WriteString("hello\n")
w.Flush()
// バッファサイズ:
// bufio.NewReaderSize(r, 64*1024)
// Scanner のデフォルト行上限は 64K:
// sc.Buffer(buf, max)
// 性能:システムコールを減らす

標準ストリーム

os.Stdin/Stdout/Stderr が標準ストリームです。fmt.Scan や bufio.Scanner で入力を受け付けます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"bufio"
"fmt"
"os"
)
// 出力:
fmt.Println("hello")
// stderr へ:
fmt.Fprintln(os.Stderr, "error")
// 入力の読み取り:
var name string
fmt.Scanln(&name)
// フォーマット付きスキャン:
var age int
fmt.Scanf("%d", &age)
// stdin を 1 行ずつ処理:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
// パイプの使い方:
// cat data | go run app.go
// 出力リダイレクト:
// go run app.go > out.txt

パス操作

filepath でプラットフォームに依存しないパス操作を行えます。Join、Dir、Base、Ext が日常的に使われます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"path/filepath"
"fmt"
)
// パスを結合:
path := filepath.Join("dir", "sub", "a.txt")
// ディレクトリ/ファイル名/拡張子:
filepath.Dir(path) // dir/sub
filepath.Base(path) // a.txt
filepath.Ext(path) // .txt
// 余分なセパレータを掃除:
filepath.Clean("dir//./a.txt")
// 絶対パス:
abs, _ := filepath.Abs(path)
// 相対パス:
rel, _ := filepath.Rel("/a", "/a/b/c")
// セパレータ:
// Windows \\ Unix /
// filepath.Separator
// マッチ走査:
// filepath.Glob("data/*.json")
// 再帰走査:filepath.WalkDir
// セパレータをハードコードしない

13.よくある落とし穴

Go の日常開発でありがちな落とし穴と正しい書き方を紹介します。

nil と空スライス

nil スライスと空スライスは別物です。JSON 化すると nil は null、空は [] になります。意図を明示しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD:nil slice は null にシリアライズ
var s []string
json.Marshal(s) // "null"
// GOOD:空の slice は [] にシリアライズ
s := make([]string, 0)
json.Marshal(s) // "[]"
// nil slice は append できる:
var n []int
n = append(n, 1) // 正常
// 判定:
// len(s) == 0 はどちらでも成立
// コレクションを返す慣例:
// 明示的に空の slice を返す:
func list() []string {
return make([]string, 0)
}

ループ変数のキャプチャ

Go 1.22 以前はループ変数を goroutine で共有する問題がありましたが、1.22 以降は反復ごとに独立します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BAD(1.22 以前):goroutine がすべて最後の値を出力
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // すべて 3 の可能性
}()
}
// GOOD:明示的に引数を渡す
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
// 1.22+ はループ変数が毎回新しい変数:
// for i := range 3 { go ... } 安全
// 旧コードとの互換の書き方:
// ローカルにコピー x := i
// 引数渡しが最も明確

マップの反復順はランダム

マップの反復順はランダムです。安定した順序が必要ならキーをソートしてから反復しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BAD:map の走査順序に依存
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v) // 順序はランダム
}
// GOOD:キーをソートして走査
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// 原因:map のハッシュ実装は順序なし
// 出力/JSON/テストでは安定した順序が必要
// slice の走査は挿入順を維持

文字列の変更

文字列は不変です。ASCII の編集は []byte に、テキスト編集は []rune に変換してから行いましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD:文字列は直接変更できない
s := "hello"
// s[0] = 'H' エラー:cannot assign
// GOOD:可変型に変換
bs := []byte(s)
bs[0] = 'H'
s = string(bs) // "Hello"
// 中国語は []rune が必要:
rs := []rune("你好")
rs[0] = '再'
s = string(rs) // "再好"
// 連結:
// + または strings.Builder
// 文字列変換にはコピーコストがある
// 大きな文字列はメモリに注意

スライスのコピーと共有

スライスの代入は基底配列を共有します。独立したコピーを作るには copy を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD:代入がデータをコピーしたと誤解
original := []int{1, 2, 3}
copySlice := original
copySlice[0] = 99
fmt.Println(original) // [99 2 3] !
// GOOD:copy でコピー
// copy のコピー先は十分な容量が必要
dst := make([]int, len(original))
copy(dst, original)
// または append パターン:
copySlice := append([]int{}, original...)
// append は共有する可能性:
// 拡張時にのみコピー
// 関数が返す slice は共有に注意
// 変更前に所有権を確認

エラーの無視

エラーを無視すると静かに失敗します。処理する、ログする、_ で意図を示す、のいずれかにしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD:エラーを無視
f, _ := os.Open("data.txt") // 失敗すると f は nil
f.Read(...) // 空ポインタ panic
// BAD:素の無視
json.Unmarshal(data, &u) // チェックしない
// GOOD:チェックして処理
f, err := os.Open("data.txt")
if err != nil {
log.Fatalf("打开失败: %v", err)
}
defer f.Close()
// 明示的に無視する意図:
_, _ = fmt.Println("x") // まれ
// 慣例:
// すべての err != nil を処理する
// コンテキストを記録してから返す

レシーバの不整合

同じ型で値レシーバとポインタレシーバが混在すると、インターフェース実装などで不整合が生じます。どちらかに統一してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BAD:レシーバの混在
func (c Counter) Get() int { return c.N }
func (c *Counter) Inc() { c.N++ } // 不整合
// 一部のメソッドがポインタのみで実装:
// 値型はインターフェースを満たさない
// GOOD:ポインタレシーバに統一
func (c *Counter) Get() int { return c.N }
func (c *Counter) Inc() { c.N++ }
// インターフェース実装:
var _ CounterIface = &Counter{} // ポインタ
// コンパイル時のアサーション:
// var _ Shape = (*Circle)(nil)
// 変更可能なオブジェクトはポインタレシーバに統一
// 小さく不変なオブジェクトは値レシーバ

変数シャドーイング

内側スコープで := を使うと新しい変数が作られ、外側をシャドーイングします。= で使い回すか明示してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD:err が遮蔽される
func f() error {
x, err := compute()
if err != nil {
return err
}
if y, err := other(); err != nil {
// err は内側の新しい変数
return err
}
_ = x
return nil
}
// 遮蔽により外側の err が未代入になる
// 検出:
// go vet が shadow を報告
// または -gcflags
// GOOD:err を再利用:
// y, err := other() 宣言済みなら再利用可
// 規則:
// 内側で外側の変数を変更するには = を使う
// 新しい変数には := を使う

ループ内の defer

ループ内の defer は外側の関数終了まで実行されません。各反復を関数化するか、明示的にクローズしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// BAD:ループ内で defer が溜まる
for _, f := range files {
fh, err := os.Open(f)
if err != nil {
continue
}
defer fh.Close() // すべて関数終了まで
}
// 大量のファイルを開いたまま閉じない
// GOOD:関数でラップ
for _, f := range files {
err := processFile(f)
if err != nil {
log.Println(err)
}
}
func processFile(name string) error {
fh, err := os.Open(name)
if err != nil {
return err
}
defer fh.Close() // 呼び出しごとに終了時に閉じる
return nil
}
// または即座に close

14.並行処理

goroutine、channel、select、並行安全について説明します。

goroutine

go キーワードで並行関数を起動します。goroutine は軽量でスタックも動的に伸びます。main が終わるとプログラムも終了します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// goroutine を起動:
go fmt.Println("并发执行")
// 匿名関数:
go func() {
fmt.Println("后台任务")
}()
// main 関数は goroutine を待たない:
// プログラムが終了するとすべての goroutine が終了
// 完了を待つには WaitGroup か channel
// 例:
go func(n int) {
fmt.Println(n * 2)
}(10)
// 注意:
// goroutine の実行順序に依存しない
// エラー処理は goroutine 内で行う

channel

チャネルは goroutine 間の通信路です。バッファなしチャネルは送受信が同期的にブロックします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// channel を作成:
ch := make(chan int)
// 送信:
ch <- 42
// 受信:
value := <-ch
// バッファなしの同期:
// 送受信双方がペアになるまでブロック
// channel を閉じる:
close(ch)
// 受信で閉じられたか判定:
v, ok := <-ch
if !ok {
fmt.Println("已关闭")
}
// 読み取り専用/書き込み専用:
// func f(ch <-chan int) // 読み取り専用
// func g(ch chan<- int) // 書き込み専用
// 単方向 channel は用途を制限

バッファ付き channel

make(chan T, n) でバッファ付きチャネルを作れます。満杯なら送信、空なら受信がブロックします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// バッファ 2:
ch := make(chan int, 2)
ch <- 1 // ブロックしない
ch <- 2 // ブロックしない
// ch <- 3 // バッファが満杯、ブロック
// 取り出して空きを作る:
<-ch
// 用途:
// タスクキュー:
work := make(chan Task, 10)
// セマフォによる流量制限:
// make(chan struct{}, 5)
// プロデューサー-コンシューマーの分離
// 注意点:
// バッファはブロックのタイミングにのみ影響
// 閉じた後の送信は panic
// バッファ内の値を取り出してから閉じたことを確認

select

select で複数のチャネル操作を同時に待ちます。複数の case が準備できたらランダムに選ばれ、default で非ブロックにできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
select {
case v := <-ch1:
fmt.Println("ch1", v)
case v := <-ch2:
fmt.Println("ch2", v)
case ch3 <- 1:
fmt.Println("发送成功")
default:
fmt.Println("无就绪,非阻塞")
}
// 複数が準備済みならランダムに 1 つ選ぶ
// 空の select は永久にブロック:
// select {}
// タイムアウトのパターン:
select {
case v := <-data:
fmt.Println(v)
case <-time.After(2 * time.Second):
fmt.Println("超时")
}
// ループで select を使い複数ストリームを処理

WaitGroup

sync.WaitGroup で goroutine の完了を待ちます。先に Add、終了時に defer Done、最後に Wait を呼びます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"
var wg sync.WaitGroup
// タスクを起動:
for i := 0; i < 5; i++ {
wg.Add(1) // カウント +1
go func(n int) {
defer wg.Done() // 完了 -1
fmt.Println(n)
}(i)
}
// すべて完了を待つ:
wg.Wait()
// 規則:
// Add は goroutine の前に呼ぶ
// Done は defer で実行を保証
// Wait 後はカウントを再利用できない
// 大量のタスクはループ外で Add(n)
// Add 後に WaitGroup をコピーしない

ミューテックス

sync.Mutex で共有データを保護します。読み込み中心なら sync.RWMutex を使うと並行性が上がります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "sync"
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock() // アンロックを保証
balance += amount
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
// 読み書きロック:
// var rw sync.RWMutex
// rw.RLock() 読み取りロック
// rw.RUnlock()
// 読み取りは並行可、書き込みは排他
// デッドロック防止:
// ロック順序を統一
// ロック保持中に時間のかかる操作をしない

sync.Once

sync.Once は関数を 1 度だけ実行することを保証します。遅延シングルトンや並列安全な初期化に使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import "sync"
var (
once sync.Once
instance *Config
)
// シングルトンパターン:
func GetConfig() *Config {
once.Do(func() {
instance = &Config{
Env: os.Getenv("APP_ENV"),
}
// 一度だけ実行
})
return instance
}
// 特徴:
// 複数 goroutine で並行安全
// 一度だけ実行(成功/失敗に関わらず)
// 用途:
// データベース接続、キャッシュ読み込み
// レジストリ初期化
// init との違い:
// init はパッケージ読み込み時に必ず実行
// Once は必要に応じて、テストでリセット可能

データ競合の検出

go run -race(または go test -race)で同期されていない並列アクセスを検出できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 検出:
// go run -race main.go
// go test -race ./...
// レースの例:
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // ロックなし、レース
}()
}
// 出力:
// WARNING: DATA RACE
// 修正:
// ロック、channel、または atomic を使う:
var c atomic.Int64
c.Add(1)
// ベストプラクティス:
// テストでは常に -race を有効に
// CI に race チェックを組み込む
// 値を共有せずメッセージ渡しを使う

15.ネットワーク

HTTP リクエスト、サーバー、context、TCP を扱います。

HTTP GET

http.Get で GET を送ります。resp.Body は defer で必ず閉じ、ステータスコードを確認しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "net/http"
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // 必ず閉じる
// ステータスコードを確認:
if resp.StatusCode != http.StatusOK {
log.Fatalf("状态码 %d", resp.StatusCode)
}
// レスポンスボディを読む:
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
// レスポンスヘッダー:
contentType := resp.Header.Get("Content-Type")
// 注意:
// デフォルトはタイムアウトなし、http.Client を使う
// 大きなレスポンスは読み取り量を制限

HTTP サーバー

http.HandleFunc でハンドラを登録し、ListenAndServe で待ち受けます。Graceful shutdown には *http.Server を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ok": true}`)
})
// 8080 で待ち受け:
log.Fatal(http.ListenAndServe(":8080", nil))
}
// フォーム読み取り:r.FormValue("name")
// JSON を返す:json.NewEncoder(w).Encode(v)
// グレースフル終了:http.Server + Shutdown

HTTP クライアント

http.Client でタイムアウトと接続プールを設定します。http.NewRequest でメソッド・ヘッダー・ボディを自由に組み立てられます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"net/http"
"time"
)
// タイムアウト付きクライアント:
client := &http.Client{
Timeout: 10 * time.Second,
}
// カスタムリクエスト:
req, _ := http.NewRequest("POST",
"https://api.example.com",
strings.NewReader(`{"name":"go"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token")
// 送信:
resp, err := client.Do(req)
// コネクションプールの再利用:
// グローバルで 1 つの Client を共有
// Transport の設定:
// MaxIdleConnsPerHost
// タイムアウトの種類:
// Timeout はレスポンス読み取りを含む

JSON API

JSON API レスポンスを構造体にデコードします。フィールドタグで API フィールド名を Go の名前にマッピングします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func fetchUsers() ([]User, error) {
resp, err := http.Get("https://api.example.com/users")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("状态码 %d", resp.StatusCode)
}
var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return nil, err
}
return users, nil
}
// 大きなレスポンスは Decoder でストリーム復号
// 未知のフィールドはデフォルトで無視
// サーバーが返すフィールド名はタグに一致

context

context はキャンセルと期限を伝播します。WithTimeout / WithCancel で派生し、I/O に貫流させましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "context"
// タイムアウト付き:
ctx, cancel := context.WithTimeout(
context.Background(), 2*time.Second)
defer cancel() // リソースを解放
// context 付きリクエストを発行:
req, _ := http.NewRequestWithContext(
ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
// キャンセル可能:
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(100 * time.Millisecond)
cancel() // すべてのサブタスクをキャンセル
}()
// 伝播:
// context は関数の第一引数にする
// 構造体フィールドに保存しない
// 派生:WithValue でリクエストメタデータを渡す

URL パース

url.Parse で URL を分解し、url.Values.Encode でクエリ文字列を構築します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "net/url"
u, err := url.Parse(
"https://user:[email protected]/path?q=go#sec")
// 各セグメント:
u.Scheme // "https"
u.Host // "example.com"
u.Path // "/path"
u.Fragment // "sec"
// クエリパラメータ:
q := u.Query()
q.Get("q") // "go"
// URL を構築:
base, _ := url.Parse("https://api.com")
base.Path = "/v1/users"
params := url.Values{}
params.Set("page", "2")
base.RawQuery = params.Encode()
// エンコード:
// url.QueryEscape / PathEscape
// 結合には ResolveReference

TCP ソケット

net.Dial で接続し、net.Listen で待ち受けます。コネクションはバイトストリームの io.ReadWriteCloser です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"net"
"bufio"
)
// クライアント:
conn, err := net.Dial("tcp", "example.com:80")
defer conn.Close()
// HTTP リクエストを送信:
fmt.Fprintf(conn,
"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
// レスポンスを読み取る:
resp, _ := bufio.NewReader(conn)
.ReadString('\n')
fmt.Println(resp)
// サーバー:
ln, _ := net.Listen("tcp", ":8080")
for {
conn, _ := ln.Accept()
go handleConn(conn) // 接続ごとに goroutine
}
// 書き込み:conn.Write([]byte)
// タイムアウト設定:conn.SetDeadline

ルーターの強化

Go 1.22 以降の http.ServeMux はメソッドとパス変数({id})をサポートします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "net/http"
// Go 1.22+ のルーティング:
mux := http.NewServeMux()
// メソッド + パス:
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
// パスパラメータ:
mux.HandleFunc("GET /users/{id}", getUser)
// ワイルドカード:
mux.HandleFunc("/files/{path...}", serveFile)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // パラメータを取得
fmt.Fprintf(w, "用户 %s", id)
}
// 旧い書き方:
// http.HandleFunc("/users/", handler)
// 手動で r.URL.Path を解析
// メソッド不一致は 405 を返す

16.時間と日付

time パッケージ、Duration、フォーマット、タイムゾーンについて説明します。

現在時刻の取得

time.Now で現在時刻を取得し、各フィールドにアクセスできます。.UTC() / .In(loc) でタイムゾーンを変換します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "time"
now := time.Now() // ローカル時刻
// 各フィールド:
now.Year() // 2026
now.Month() // August
now.Day() // 2
now.Hour()
now.Minute()
now.Second()
now.Weekday() // Sunday
// UTC:
t := time.Now().UTC()
// Unix タイムスタンプ:
unix := time.Now().Unix() // 秒
milli := time.Now().UnixMilli() // ミリ秒
// 特定の時刻:
// time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
// 比較:t.Before(u) / t.After(u)

Duration

time.Duration はナノ秒の整数です。time.Second などの定数と組み合わせ、加減やスリープに使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "time"
// 定数:
// time.Nanosecond
// time.Microsecond
// time.Millisecond
// time.Second
// time.Minute
// time.Hour
// 期間を定義:
d := 2 * time.Second
// 変換:
ms := d.Milliseconds()
ns := d.Nanoseconds()
// フォーマット:d.String() // "2s"
// 解析:
d, _ = time.ParseDuration("1h30m")
// 計算:
// t.Add(d) / t.Sub(u)
// t.AddDate(0, 1, 0) 1 か月加算
// 待機:
// time.Sleep(d)
// 比較:
// d > time.Minute
// 型安全:int で直接演算できない

フォーマット

Time.Format はレイアウト文字列に従って整形します。リファレンスタイム「2006-01-02 15:04:05」を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "time"
t := time.Now()
// 標準フォーマット:
t.Format("2006-01-02") // 2026-08-02
t.Format("15:04:05") // 14:30:22
t.Format("2006-01-02 15:04") // 完全形
t.Format("2006-01-02 15:04:05 MST") // タイムゾーン付き
// 参考時刻は固定:
// 01月 02日 03時 04分 05秒
// 06年 07月 MST
// 2006 年が重要な目印
// 定義済み:
// time.RFC3339:"2006-01-02T15:04:05Z07:00"
// time.RFC1123 / time.Kitchen
// レイアウトが間違うと空文字列になる

パース

time.Parse は同じリファレンスレイアウトで文字列を Time に変換します。タイムゾーンには ParseInLocation を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "time"
// レイアウトで解析:
t, err := time.Parse("2006-01-02", "2026-08-02")
// タイムゾーン付き:
// time.Parse(time.RFC3339, "2026-08-02T10:00:00Z")
// ローカルタイムゾーンで解析:
// time.ParseInLocation(layout, value, time.Local)
// よくあるエラー:
// レイアウトを間違えるとゼロ値を返す
// 月/日は参考値を使うこと
// 使い方:
// time.Parse("2006/01/02 15:04", "2026/08/02 10:30")
// タイムゾーン処理:
// タイムゾーンなしは UTC として解析
// ローカルタイムゾーンが必要なら ParseInLocation

Ticker

time.Ticker は一定間隔で発火します。不要になったら defer ticker.Stop() でリソースを解放します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "time"
// 毎秒トリガー:
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 解放
// 定期実行:
for range ticker.C {
fmt.Println("tick", time.Now())
}
// 手動制御:
// 一度だけ消費:
// <-ticker.C
// 停止チェック:
for {
select {
case t := <-ticker.C:
fmt.Println("执行", t)
case <-stop:
return // 終了
}
}
// 注意:
// Stop を忘れるとタイマーがリーク
// バッファなしの C は遅れると破棄
// 略記:time.Tick は解放されないので慎重に

Sleep と待機

time.Sleep はその goroutine を指定時間ブロックします。キャンセル可能な遅延には Timer / After を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "time"
// 2 秒停止:
time.Sleep(2 * time.Second)
// 動的な期間:
wait := time.Duration(n) * time.Millisecond
time.Sleep(wait)
// ミリ秒の値:
time.Sleep(500 * time.Millisecond)
// 注意:
// 現在の goroutine をブロック
// 正確なスケジューリングには使わない
// テストでの高速化:
// 期間を変数に抽出して差し替えやすく
// Timer との違い:
// Sleep は無条件にブロック
// Timer はキャンセル可能
// select で Sleep をタイムアウトに使わない

Timer

time.Timer は指定時間後に 1 度だけ発火します。Stop / Reset で制御し、select と組み合わせてキャンセルできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "time"
// 3 秒後にトリガー:
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
// トリガーを待つ:
<-timer.C
fmt.Println("定时到")
// 早期キャンセル:
// timer.Stop()
// キャンセルされたか判定:
// select を使う:
select {
case <-timer.C:
fmt.Println("执行")
case <-ctx.Done():
fmt.Println("已取消")
}
// リセット:
// timer.Reset(5 * time.Second)
// 一回きり vs Ticker の繰り返し
// 用途:タイムアウト制御、遅延タスク
// After は Timer の便利なラッパー

タイムゾーン

IANA タイムゾーンは time.LoadLocation で読み込み、.In(loc) で適用します。time.FixedZone で固定オフセットも作れます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "time"
// タイムゾーンを読み込む:
loc, err := time.LoadLocation("Asia/Shanghai")
// そのタイムゾーンに変換:
t := time.Now().In(loc)
// よく使うタイムゾーン:
// time.UTC、time.Local
// "America/New_York"
// "Europe/London"
// "Asia/Tokyo"
// 固定オフセット:
fixed := time.FixedZone("CST", 8*3600)
// 特定のタイムゾーンで構築:
// time.Date(2026, 8, 2, 10, 0, 0, 0, loc)
// 注意:
// IANA データベースがないとエラー
// Windows は tzdata のインポートが必要:
// import _ "time/tzdata"
// 時刻は UTC で保存するのが推奨

17.プロセスと環境

コマンドライン引数、環境変数、サブプロセス、終了コードについて説明します。

コマンドライン引数

os.Args はコマンドライン引数のスライスです。os.Args[0] はプログラムパス、残りはユーザー引数になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"fmt"
"os"
)
// すべての引数:
// go run app.go a b c
// os.Args = ["app.go", "a", "b", "c"]
for i, arg := range os.Args {
fmt.Printf("args[%d] = %s\n", i, arg)
}
// 業務用の引数のみ:
args := os.Args[1:]
// シンプルな解析:
if len(args) < 1 {
fmt.Println("用法: app <name>")
os.Exit(1)
}
name := args[0]
// 注意:
// 空白区切り、引用符で空白を含められる
// 複雑な引数は flag パッケージ
// 引数は文字列

flag パース

flag パッケージでコマンドラインフラグを解析します。デフォルト値を指定し、Parse 後に値を読みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "flag"
// フラグを定義:
name := flag.String("name", "Go", "用户名")
count := flag.Int("n", 1, "次数")
verbose := flag.Bool("v", false, "详细输出")
// 解析:
flag.Parse()
// 使用:
for i := 0; i < *count; i++ {
fmt.Printf("%s (%v)\n", *name, *verbose)
}
// 実行:
// go run app.go -name Nick -n 3 -v
// フラグ以外の引数:
rest := flag.Args()
// カスタム Usage:
flag.Usage = func() { fmt.Println("用法...") }
// 解析エラーの確認:
// flag.Parse はエラーで自動終了
// 略記:
// flag.StringVar(&s, "x", "", "说明")

環境変数

os.Getenv で読み取り、os.Setenv で設定、os.LookupEnv で未設定と空文字を区別します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"fmt"
"os"
)
// 読み取り:
path := os.Getenv("PATH")
// デフォルト値付き:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// 存在するか判定:
val, ok := os.LookupEnv("APP_ENV")
if !ok {
fmt.Println("未设置 APP_ENV")
}
// 設定:
os.Setenv("APP_ENV", "production")
// 削除:
os.Unsetenv("APP_ENV")
// すべてを一覧:
// os.Environ() // ["KEY=value", ...]
// 展開:os.ExpandEnv("$HOME/x")
// 設定は環境変数を優先

終了コード

os.Exit は指定した終了コードで直ちに終了します。0 が成功、0 以外が失敗です。defer は実行されません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "os"
// 成功終了:
// 正常な return の終了コードは 0
// 失敗終了:
if err != nil {
fmt.Fprintln(os.Stderr, "失败:", err)
os.Exit(1)
}
// 注意:
// os.Exit は defer を実行しない
// バッファ付き出力をフラッシュしない
// 慣例:
// 0 成功
// 1 一般的なエラー
// 2 用法エラー
// 64-113 予約/シグナル
// ライブラリコードで os.Exit を呼ばない
// main でだけ使う
// log.Fatal も終了する:
// log.Fatalf("错误: %v", err) // 退出码 1

サブプロセスの実行

exec.Command で外部コマンドを実行します。stdout / stderr を取得したり、stdin をパイプしたり、環境変数を設定できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"os/exec"
"strings"
)
// 実行して出力を取得:
out, err := exec.Command("ls", "-l").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
// 結合出力(stderr を含む):
// cmd.CombinedOutput()
// stderr を捕捉:
var stderr strings.Builder
cmd := exec.Command("go", "version")
cmd.Stderr = &stderr
// 環境を設定:
cmd.Env = append(os.Environ(), "FOO=bar")
// 対話入力:
// cmd.Stdin = strings.NewReader("data")
// ストリーム出力:
// cmd.Stdout = os.Stdout
// 実行状態:cmd.Run()

シグナル処理

signal.Notify で OS シグナルをチャネルに届けます。SIGINT / SIGTERM を処理してGraceful shutdown を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"os"
"os/signal"
"syscall"
)
// シグナルを監視:
quit := make(chan os.Signal, 1)
signal.Notify(quit,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM) // kill
// シグナルを待つ:
<-quit
fmt.Println("收到退出信号,清理资源")
// 状態を保存、接続を閉じ、タスクを停止
os.Exit(0)
// 無視のみ:
// signal.Ignore(syscall.SIGINT)
// 監視を停止:
// signal.Stop(quit)
// よくある用途:
// サービスのグレースフルシャットダウン、CLI ツールの中断

ファイルシステム

MkdirAll でディレクトリ作成、RemoveAll でツリー削除、Rename で移動、Stat で情報を取得します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "os"
// ディレクトリ作成(親を含む):
os.MkdirAll("data/sub", 0755)
// ディレクトリツリーを削除:
os.RemoveAll("data")
// ファイルを削除:
os.Remove("tmp.txt")
// リネーム/移動:
os.Rename("a.txt", "b.txt")
// ファイル情報:
info, err := os.Stat("data.txt")
if err == nil {
fmt.Println(info.Size())
fmt.Println(info.IsDir())
fmt.Println(info.ModTime())
}
// 存在するか:
if _, err := os.Stat("f"); os.IsNotExist(err) {
fmt.Println("不存在")
}
// 一時ファイル:
// os.CreateTemp("", "prefix-*")
// ディレクトリ走査:os.ReadDir("dir")

作業ディレクトリ

os.Getwd で現在の作業ディレクトリを取得し、os.Chdir で変更します。filepath.Abs で絶対パスに変換しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"os"
"path/filepath"
)
// 現在のディレクトリ:
cwd, err := os.Getwd()
fmt.Println(cwd)
// 切り替え:
// os.Chdir("data")
// パスを結合:
path := filepath.Join(cwd, "config", "app.yaml")
// クリーンアップ/解決:
// filepath.Clean(path)
// filepath.Abs(path)
// よく使う:
// 設定の相対パス読み込み
// キャッシュディレクトリ:
// os.UserCacheDir()
// 設定ディレクトリ:
// os.UserConfigDir()
// 実行ファイルのパス:
// os.Executable()
// プログラムの実行ディレクトリとソースディレクトリは異なる点に注意

18.正規表現

regexp パッケージ:コンパイル、マッチ、検索、置換、キャプチャグループについて説明します。

コンパイル

regexp.Compile はエラーを返します。regexp.MustCompile は失敗時にパニックし、パッケージスコープでの定義に向きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "regexp"
// コンパイル(エラーを返すことがある):
re, err := regexp.Compile(`^[a-z]+$`)
if err != nil {
log.Fatal(err)
}
// 固定パターンは直接 MustCompile:
re = regexp.MustCompile(`\d+`)
// コンパイルはコストが高い、ループ外で:
// グローバル変数:
var emailRe = regexp.MustCompile(
`^[\w.+-]+@[\w-]+\.[\w.]+$`)
// 簡略化:
// regexp.MatchString は毎回コンパイル
// 高頻度で呼ぶなら事前コンパイル
// コンパイル後は並行安全で再利用可

マッチ判定

MatchString は文字列マッチ、Match は []byte マッチを判定します。完全一致は ^...$ で囲みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
var digitRe = regexp.MustCompile(`\d+`)
// 文字列を判定:
digitRe.MatchString("abc123") // true
// バイトをマッチ:
digitRe.Match([]byte("abc123")) // true
// 完全一致:
// ^...$ でパターンを囲む
re := regexp.MustCompile(`^\d{3}$`)
re.MatchString("123") // true
re.MatchString("1234") // false
// 大文字小文字:
// (?i) インラインフラグ
// regexp.MustCompile(`(?i)^go$`)
// 説明:
// デフォルトは貪欲マッチ
// 空マッチもマッチ
// 用途:フォーム検証、ログフィルタ

検索

FindString は最初のマッチを返し、FindStringIndex はバイト位置を返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "regexp"
var re = regexp.MustCompile(`\bgo\b`)
// 最初のマッチ:
re.FindString("I love go and Go") // "go"
// バイト:re.Find([]byte(s))
// マッチ位置:
idx := re.FindStringIndex("go and go")
// idx = [0 2]
// すべての位置:
all := re.FindAllStringIndex("go and go", -1)
// [[0 2] [8 10]]
// 数を制限:
// re.FindAllString(s, 2) 最大 2 個
// 部分マッチ文字列:
// re.FindStringSubmatch(s)
// マッチがあるか判定:
// re.FindString(s) != ""

全マッチ

FindAllString で全マッチを取得できます。-1(または省略)で無制限に、整数で上限を指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
var re = regexp.MustCompile(`\d+`)
// すべてのマッチ:
all := re.FindAllString("a1 b22 c333", -1)
// ["1" "22" "333"]
// 最初の 2 個に制限:
re.FindAllString(s, 2)
// カウント:
// len(all)
// バイト版:
// re.FindAll(s, -1)
// 位置:
// re.FindAllStringIndex(s, -1)
// 各グループのマッチ:
// re.FindAllStringSubmatch(s, -1)
// 例:すべてのメールを抽出
// 注意:
// マッチなしは nil スライスを返す
// -1 は数の上限なし

置換

ReplaceAllString で全マッチを置換します。$1, $2 でキャプチャグループを参照できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
var re = regexp.MustCompile(`\d+`)
// すべての数字を置換:
re.ReplaceAllString("a1b2", "#")
// "a#b#"
// グループ参照:
var nameRe = regexp.MustCompile(
`(\w+),\s*(\w+)`)
nameRe.ReplaceAllString("Doe, John", "$2 $1")
// "John Doe"
// 回数制限:
// re.ReplaceAllString(s, "x") 回数パラメータなし
// ReplaceAllLiteralString は展開しない:
// re.ReplaceAllLiteralString(s, "$1")
// コールバック:
// re.ReplaceAllStringFunc(s, fn)
// 例:数字を 1 加算
// マッチなしは元の文字列を返す

分割

re.Split でパターンに基づき文字列を分割できます。-1 で無制限になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
// カンマで分割:
var re = regexp.MustCompile(`,`)
parts := re.Split("a,b,c", -1)
// ["a" "b" "c"]
// 空白で分割:
spaces := regexp.MustCompile(`\s+`)
spaces.Split("a b\tc", -1)
// ["a" "b" "c"]
// 数を制限:
re.Split(s, 2) // 最大 2 セグメント
// 注意:
// -1 は制限なし
// 空マッチの分割は特殊な挙動
// 代替:
// strings.Fields は空白で
// strings.Split はリテラルで
// 複雑なパターンだけ正規表現を使う
// 分割結果の空セグメント除去は自分でフィルタ

キャプチャグループ

括弧でサブマッチを抽出します。FindStringSubmatch で取得し、名前付きグループ (?P<name>) も使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
// キャプチャグループ:
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
sub := re.FindStringSubmatch("2026-08-02")
// sub[0] = "2026-08-02" 全体
// sub[1] = "2026"
// sub[2] = "08"
// sub[3] = "02"
// 名前付きグループ:
nameRe := regexp.MustCompile(`(?P<year>\d{4})`)
sub = nameRe.FindStringSubmatch("2026")
idx := nameRe.SubexpIndex("year") // 1
sub[idx] // "2026"
// すべてのグループ:
// re.FindAllStringSubmatch(s, -1)
// 非キャプチャグループ:
// (?:...) はインデックスを使わない
// グループ数を確認:re.NumSubexp()

よく使うパターン

よく使う正規表現(メール、URL、IP、電話番号など)と基本構文(\d、\w、\s、量詞、^、$)をまとめます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
// メール:
regexp.MustCompile(`^[\w.+-]+@[\w-]+\.[\w.]+$`)
// URL:
regexp.MustCompile(`^https?://[\w.-]+`)
// 携帯番号(中国):
regexp.MustCompile(`^1[3-9]\d{9}$`)
// 身分証番号:
regexp.MustCompile(`^\d{17}[\dXx]$`)
// IP アドレス:
regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`)
// 構文の要点:
// \d 数字 \w 単語 \s 空白
// + 1 個以上 * 0 個以上
// {2,4} 回数の範囲
// ^ 行頭 $ 行末
// 貪欲から非貪欲:\d+? に ? を付ける
// 反対:\D \W \S
// 文字クラス:[a-zA-Z] [^0-9]

19.ビルドとテスト

ビルド、単体テスト、静的検査、クロスプラットフォームコンパイルを扱います。

go build

go build でパッケージをコンパイルします。-o、-tags、-ldflags などで出力やオプションを制御できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 現在のディレクトリをビルド:
// go build
// 出力先を指定:
// go build -o bin/app .
// 複数のパッケージ:
// go build ./...
// コンパイルのみで出力なし(検査):
// go build ./...
// ビルドタグ:
// go build -tags "jsoniter"
// サイズを縮小:
// go build -ldflags "-s -w"
// バージョン情報を埋め込む:
// go build -ldflags "-X main.version=1.2.3"
// クロスコンパイルは次節
// キャッシュ:
// 増分ビルドは build cache を使う
// 検証:
// go build && go vet

単体テスト

go test が *_test.go を実行します。t.Error / t.Errorf で非致命、t.Fatal / t.Fatalf でそのテストを停止します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// テストファイル:calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
got := add(1, 2)
if got != 3 {
t.Errorf("add(1,2) = %d; want 3", got)
}
}
// テーブル駆動テスト:
func TestAddTable(t *testing.T) {
cases := []struct{ a, b, want int }{
{1, 2, 3}, {0, 0, 0}, {-1, 1, 0},
}
for _, c := range cases {
if got := add(c.a, c.b); got != c.want {
t.Errorf("add(%d,%d)=%d; want %d",
c.a, c.b, got, c.want)
}
}
}
// 実行:
// go test // 現在のパッケージ
// go test ./... // すべて
// go test -v -run TestAdd
// カバレッジ:go test -cover

ベンチマーク

go test -bench で Benchmark 関数を実行します。フレームワークが b.N 回ループしてオペレーション単価を計測します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "testing"
func BenchmarkConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
concat("a", "b") // 測定対象の関数
}
}
// 実行:
// go test -bench=. -benchmem
// 出力:
// BenchmarkConcat-8 42984412
// 27.36 ns/op 16 B/op 2 allocs/op
// オプション:
// -bench=Concat 名前でマッチ
// -benchmem メモリ割り当てを表示
// -count=3 複数回実行
// -benchtime=5s 固定時間
// 最適化の比較:
// 最適化前後で各 1 回実行して比較
// 注意:
// コンパイラがインライン化/定数畳み込みする可能性
// ウォームアップが目立たない場合は無視可

静的解析

go vet はありがちなミス(printf の型不一致、ロックの誤用など)を検出します。CI で必ず実行しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 現在のパッケージを検査:
// go vet
// すべてを検査:
// go vet ./...
// よくある検出:
// 1. フォーマット文字列エラー:
// fmt.Printf("%d", "x") 型不一致
// 2. ロック使用の誤り:
// sync.Mutex をコピー
// 3. 無用な代入
// 4. 文字列エスケープの問題
// 5. オフセット/インデックス範囲外
// 他のツール:
// golangci-lint はより多くの検査を集約
// 静的解析:
// go build -gcflags=-m でインライン化を確認
// ベストプラクティス:
// コミット前に必ず実行
// CI に go vet ./... を組み込む

フォーマット

gofmt が公式フォーマッタです。goimports を併用するとインポートの自動整理もできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 単一ファイルをフォーマット:
// gofmt -w file.go
// すべてをフォーマット:
// gofmt -w .
// 検査のみで変更しない:
// gofmt -l .
// ディレクトリを再帰:
// gofmt -l ./...
// インポートの並べ替え:
// goimports -w file.go
// エディタ統合:
// 保存時に自動フォーマット
// チーム協力:
// 統一フォーマットでスタイル論争なし
// 注意:
// gofmt はフォーマットのみでロジックは変更しない
// CI チェック:
// test -z "$(gofmt -l .)"
// タブとスペースの混在は自動修正

クロスコンパイル

GOOS と GOARCH を指定して go build を実行するとクロスコンパイルできます。CGO を有効にする場合は対応するクロスの C ツールチェインが必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Windows をビルド:
// GOOS=windows GOARCH=amd64 go build .
// Linux をビルド:
// GOOS=linux GOARCH=amd64 go build .
// macOS をビルド:
// GOOS=darwin GOARCH=arm64 go build .
// 32 ビット:
// GOOS=linux GOARCH=386
// ARM:
// GOOS=linux GOARCH=arm64
// 出力名にサフィックス:
// go build -o app.exe .
// 利用可能なプラットフォーム組み合わせ:
// go tool dist list
// 注意:
// CGO はデフォルトで無効(純 Go のみクロス可)
// クロスコンパイルにターゲットの環境は不要
// 一般的な組み合わせでリリースマトリックス

競合検出

go build -race(または go test -race)でランタイムのデータ競合検出を有効化できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 検出付きで実行:
// go run -race main.go
// 検出付きでテスト:
// go test -race ./...
// 検出付きでビルド:
// go build -race
// 検出内容:
// 複数の goroutine が同時に読み書き
// 同じメモリアドレス
// 同期保護なし
// 出力例:
// WARNING: DATA RACE
// Read at 0x... by goroutine 5
// 修正方法:
// ミューテックスロック / channel を使う
// atomic 操作を使う
// 注意:
// 実行時パスのみ検出
// カバレッジはテストに依存
// 本番バイナリに -race を使わない
// CI で常時 -race を有効に

モジュールバージョン

go.mod でバージョンを固定し、go get で更新、replace で別モジュールへ向け、go.sum で整合性を検証します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 依存を追加:
// go get github.com/user/[email protected]
// 最新に更新:
// go get -u ./...
// major アップグレード:
// go get module/v2@latest
// replace でローカルデバッグ:
// replace github.com/foo/bar => ../bar
// または fork を指定:
// replace foo => github.com/user/foo v1.0.0
// 未使用を削除:
// go mod tidy
// チェックサム:
// go mod verify
// バージョン固定:
// go.mod の require ブロックを見る
// go.sum で整合性を保証:
// 依存のハッシュを検証

CI 連携

CI ではフォーマット、go vet、go test -race -cover、ビルド、アーティファクト公開を順に実行するのが一般的です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 典型的な CI 手順:
// 1. コードをチェックアウト
// 2. Go をインストール:
// actions/setup-go
// 3. モジュールをキャッシュ:
// $HOME/go/pkg/mod
// 4. フォーマットを検査:
// gofmt -l .
// 5. 静的検査:
// go vet ./...
// 6. テスト:
// go test -race -cover ./...
// 7. ビルド:
// go build ./...
// 8. 成果物を公開:
// 複数プラットフォームのバイナリをクロスビルド
// マトリックス:
// Go バージョン 1.21 / 1.22
// OS linux/windows/darwin
// しきい値:
// テスト失敗は即失敗
// カバレッジ閾値は任意

公式リンク

公式ドキュメントとリソースへの直接リンク。

このチートシートについて

このページは Go 1.22 の自己完結型クイックリファレンスです。言語コアと、実プロジェクトで日常の約 80% を占める標準ライブラリを扱います。内容は現代のイディオムを重視しており、短変数宣言、errors.Is / errors.As による構造化エラー、ジェネリクス、Go 1.22 からの反復ごとに独立したループ変数を取り上げます。信頼できるリファレンスとしては、公式 Go チュートリアルと Effective Go を参照してください。 19 のセクションはそれぞれ 1 つのテーマに焦点を当てており、最初のプログラムから goroutine、インターフェース、そしてよくある落とし穴までを順にカバーします。各セクションは 8〜14 のサブトピック(5〜20 行ずつのコード例)に分かれており、合計でおよそ 150 トピックあります。コードは意図的に短く自己完結的にしてあります。 すべての処理はブラウザ内で行われます。アップロードも追跡もありません。このページは GuruToolkit の無料開発者向けツール集の一部で、掲載されているコードは自由に使えますが、いかなる保証もありません。

バージョン 2.1.0