【第1回】Go の基本文法を完全に理解する ── Go バックエンド開発ロードマップ

Go
B!

シリーズ構成

  1. Go の基本文法(本記事)
  2. CLI ツールを作る
  3. HTTP サーバーと REST API
  4. データベース連携
  5. 実務的な周辺技術(Docker・設定・ログ・認証・並行処理)
  6. ポートフォリオを作る
  7. 実践知識を深める

この回の目標は 「Go の文法要素をすべて触り、他人が書いた Go のコードを詰まらずに読める状態になる」 ことです。分量は多いですが、すべて手で打ち込んで実行してください。写経の速度より、ひとつひとつの挙動を確かめることを優先します。


  1. 目次
  2. 1. 環境構築と最初のプログラム
    1. インストール
    2. エディタ設定
    3. Hello, World
  3. 2. パッケージとモジュール
    1. モジュール
    2. パッケージ
    3. import の書き方
  4. 3. 変数・定数・型
    1. 宣言の 3 パターン
    2. ゼロ値
    3. 基本型
    4. 定数と iota
    5. 型定義と型エイリアス
  5. 4. 関数
    1. 基本形と多値返却
    2. エラーを返す関数
    3. 可変長引数
    4. 関数は値
  6. 5. 制御構文
    1. if
    2. for
    3. switch
  7. 6. 配列・スライス・マップ
    1. 配列
    2. スライス
      1. 重要:スライスは底層配列を共有する
      2. 要素の削除
      3. nil スライスと空スライス
      4. slices パッケージ(Go 1.21+)
    3. マップ
  8. 7. 文字列と rune
    1. よく使う strings / strconv
    2. fmt の書式
  9. 8. 構造体とメソッド
    1. 構造体の定義と初期化
    2. メソッド
      1. レシーバの選び方
    3. コンストラクタ
    4. 埋め込み(合成)
    5. 構造体タグ
    6. 構造体の比較
  10. 9. ポインタ
    1. 値渡しとポインタ渡し
    2. new と &T{}
    3. nil ポインタ
    4. ポインタ演算はない
  11. 10. インターフェース
    1. 暗黙的な実装
    2. メソッドセットとポインタレシーバ
    3. 空インターフェースと any
    4. 標準ライブラリの小さなインターフェース
    5. インターフェースの合成
    6. 設計の指針
    7. インターフェースの nil
  12. 11. エラー処理
    1. 基本パターン
    2. エラーの生成
    3. ラップと Is / As
    4. エラーメッセージの書き方
    5. 複数エラーの結合(Go 1.20+)
    6. エラー処理の判断フロー
  13. 12. defer・panic・recover
    1. defer
    2. panic と recover
  14. 13. クロージャ
    1. ループ変数の捕捉(Go 1.22 で修正)
  15. 14. ジェネリクス
    1. 制約(constraints)
    2. ジェネリック型
    3. 使いすぎない
  16. 15. goroutine と channel(入門)
    1. goroutine
    2. channel
    3. sync.WaitGroup
  17. 16. よくある落とし穴
    1. シャドーイング
    2. スライスの共有
    3. マップへの並行書き込み
    4. nil マップへの書き込み
    5. 未使用変数・import
    6. := による意図しない新規宣言
    7. 構造体を値でループすると変更が反映されない
    8. time.Duration の単位
  18. 17. 開発ツール
  19. 18. 演習問題
    1. 基礎
    2. 構造体・インターフェース
    3. エラー処理
    4. 応用
    5. 次回予告

目次

  1. 環境構築と最初のプログラム
  2. パッケージとモジュール
  3. 変数・定数・型
  4. 関数
  5. 制御構文
  6. 配列・スライス・マップ
  7. 文字列と rune
  8. 構造体とメソッド
  9. ポインタ
  10. インターフェース
  11. エラー処理
  12. defer・panic・recover
  13. クロージャ
  14. ジェネリクス
  15. goroutine と channel(入門)
  16. よくある落とし穴
  17. 開発ツール
  18. 演習問題

1. 環境構築と最初のプログラム

インストール

go.dev/dl からインストーラを取得します。

# macOS
brew install go

# Linux(公式アーカイブ)
wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

go version

エディタ設定

VS Code に拡張 Go(golang.go)を入れ、初回に促される gopls などのツールをすべてインストールします。以下を settings.json に加えると保存時に整形と import 整理が自動で走ります。

{
  "[go]": {
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.organizeImports": "explicit"
    }
  }
}

Hello, World

mkdir hello && cd hello
go mod init example.com/hello
// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
}
go run .              # コンパイルして即実行
go build -o hello .   # バイナリ生成
./hello

ここで覚えること

  • 実行可能プログラムは package main に属し、func main() から始まる
  • import した標準ライブラリは追加インストール不要
  • go run は開発用、go build は配布用

2. パッケージとモジュール

モジュール

go mod init <モジュールパス> で go.mod が作られます。モジュールパスは慣習的にリポジトリの URL(github.com/yourname/hello)を使いますが、学習中は example.com/hello で構いません。

module example.com/hello

go 1.23

外部パッケージは go get で追加され、go.mod と go.sum(チェックサム)に記録されます。

go get github.com/google/uuid
go mod tidy    # 未使用の依存を削除・不足を追加

パッケージ

1 ディレクトリ = 1 パッケージ です。同じディレクトリ内のファイルは同じ package 宣言を持ち、互いの識別子を自由に参照できます。

hello/
├── go.mod
├── main.go            # package main
└── greet/
    └── greet.go       # package greet
// greet/greet.go
package greet

// 大文字始まり → エクスポート(他パッケージから見える)
func Hello(name string) string {
    return "Hello, " + name + "!"
}

// 小文字始まり → パッケージ内限定
func helper() {}
// main.go
package main

import (
    "fmt"

    "example.com/hello/greet" // モジュールパス + ディレクトリ
)

func main() {
    fmt.Println(greet.Hello("Gopher"))
}

可視性は先頭文字の大小だけで決まります。 public / private キーワードはありません。

import の書き方

import (
    "fmt"           // 標準ライブラリ
    "os"

    "github.com/google/uuid"   // 外部パッケージ(空行で区切るのが慣習)

    "example.com/hello/greet"  // 自モジュール
)

未使用の import はコンパイルエラーになります。gopls が自動で消してくれるので気にしなくて構いません。


3. 変数・定数・型

宣言の 3 パターン

package main

import "fmt"

func main() {
    var a int          // ゼロ値 0 で初期化
    var b int = 10     // 型と初期値
    var c = 10         // 型推論
    d := 10            // 短縮宣言(関数内のみ)

    var (              // まとめて宣言
        host = "localhost"
        port = 8080
    )

    fmt.Println(a, b, c, d, host, port)
}

実務では 関数内は :=、パッケージレベルは var が基本です。

ゼロ値

Go では宣言した変数は必ず初期化されます。

型ゼロ値
数値0
string""
boolfalse
ポインタ、スライス、マップ、channel、関数、インターフェースnil
構造体各フィールドがゼロ値

「ゼロ値で使える型を設計する」は Go の重要な指針です(sync.Mutex や bytes.Buffer は宣言だけで使えます)。

基本型

var (
    i   int     = -1        // 環境依存(通常 64bit)
    i64 int64   = 1 << 40
    u8  uint8   = 255       // byte の別名
    f   float64 = 3.14
    b   bool    = true
    s   string  = "文字列"
    r   rune    = 'あ'      // int32 の別名。Unicode コードポイント
)

Go は 暗黙の型変換をしません。

var x int = 10
var y float64 = 2.5
// z := x * y  → コンパイルエラー
z := float64(x) * y // 明示的に変換

定数と iota

const Pi = 3.14159
const MaxRetry int = 3

// 型なし定数は柔軟に使える
const big = 1 << 62

// iota:連番の列挙
type Weekday int

const (
    Sunday Weekday = iota // 0
    Monday                // 1
    Tuesday               // 2
)

// ビットフラグ
type Perm uint8

const (
    Read Perm = 1 << iota // 1
    Write                 // 2
    Exec                  // 4
)

型定義と型エイリアス

type UserID int64          // 新しい型(int64 とは別物)
type Celsius float64

type MyInt = int           // エイリアス(int と完全に同じ)

type UserID int64 としておくと、UserID と OrderID を取り違えたときにコンパイルエラーになります。ID の型はこのように定義するのが定番です。


4. 関数

基本形と多値返却

func add(a, b int) int {
    return a + b
}

// 複数の戻り値
func divmod(a, b int) (int, int) {
    return a / b, a % b
}

// 名前付き戻り値(短い関数では避ける。ドキュメント目的で使う)
func minmax(xs []int) (min, max int) {
    min, max = xs[0], xs[0]
    for _, x := range xs {
        if x < min {
            min = x
        }
        if x > max {
            max = x
        }
    }
    return // 裸の return は名前付き戻り値を返す
}

エラーを返す関数

Go の関数で最も多い形です。最後の戻り値を error にする のが鉄則です。

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Println(result)
}

可変長引数

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)
xs := []int{4, 5, 6}
sum(xs...) // スライスを展開して渡す

関数は値

func apply(f func(int) int, x int) int {
    return f(x)
}

double := func(n int) int { return n * 2 }
apply(double, 5) // 10

5. 制御構文

if

// 括弧なし、波括弧必須
if x > 10 {
    // ...
} else if x > 5 {
    // ...
} else {
    // ...
}

// 初期化文つき if。スコープが if ブロック内に限定される
if v, ok := m["key"]; ok {
    fmt.Println(v)
}

if err := doSomething(); err != nil {
    return err
}

for

Go にはループ構文が for しかありません。

// C スタイル
for i := 0; i < 5; i++ {}

// while
for cond {}

// 無限ループ
for {
    if done { break }
}

// range(Go 1.22 以降は整数も可)
for i := range 5 {
    fmt.Println(i) // 0..4
}

for i, v := range slice {}     // インデックスと値
for _, v := range slice {}     // 値だけ
for i := range slice {}        // インデックスだけ
for k, v := range m {}         // マップ(順序は不定)
for i, r := range "日本語" {}   // 文字列は rune 単位

// ラベル付き break/continue
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if j == 1 {
            continue outer
        }
    }
}

switch

switch day {
case "sat", "sun":     // 複数の値
    fmt.Println("weekend")
case "mon":
    fmt.Println("monday")
    fallthrough        // 明示しないと次の case に落ちない
case "tue":
    fmt.Println("early week")
default:
    fmt.Println("other")
}

// 条件式なし switch(if-else の連鎖を置き換える)
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

break は不要です。C 言語と違って 自動で抜けます。


6. 配列・スライス・マップ

配列

長さが型の一部。実務で直接使うことは少なく、スライスの土台として理解します。

var a [3]int              // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]int{1, 2, 3, 4} // 長さ推論 → [4]int

// 配列は値型。代入・引数渡しでコピーされる
d := b
d[0] = 100
fmt.Println(b[0]) // 1

スライス

スライスは 「配列への参照 + 長さ + 容量」の 3 要素構造体 です。

s := []int{1, 2, 3}
fmt.Println(len(s), cap(s)) // 3 3

s = append(s, 4)
fmt.Println(len(s), cap(s)) // 4 6(容量は自動拡張)

// make(型, 長さ, 容量)
t := make([]int, 0, 10)

// 部分スライス [start:end)。end は含まない
u := s[1:3] // [2 3]

重要:スライスは底層配列を共有する

a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3]
b[0] = 100
fmt.Println(a) // [1 100 3 4 5] ← a も変わる!

append で容量を超えると 新しい配列にコピー されるため、共有が切れます。この挙動がバグの温床になります。

a := []int{1, 2, 3}
b := a[:2]           // [1 2], cap 3
b = append(b, 99)    // 容量内 → a[2] が上書きされる
fmt.Println(a)       // [1 2 99]

安全にコピーしたいときは copy か slices.Clone を使います。

import "slices"

c := slices.Clone(a)

要素の削除

// i 番目を削除
s = append(s[:i], s[i+1:]...)

// Go 1.21+ の slices パッケージ
s = slices.Delete(s, i, i+1)

nil スライスと空スライス

var a []int        // nil。len 0
b := []int{}       // 空だが non-nil
c := make([]int, 0)

// 判定は len で行う。nil 比較はしない
if len(a) == 0 {}

JSON にすると nil は null、空スライスは [] になります。API のレスポンスでは意識が必要です。

slices パッケージ(Go 1.21+)

import "slices"

xs := []int{3, 1, 2}
slices.Sort(xs)                    // [1 2 3]
slices.Contains(xs, 2)             // true
idx := slices.Index(xs, 3)         // 2
slices.Reverse(xs)
m := slices.Max(xs)

マップ

// 宣言と初期化
ages := map[string]int{"alice": 30}
scores := make(map[string]int)

var m map[string]int   // nil マップ。読み取りは OK、書き込みは panic!
// m["a"] = 1  → panic: assignment to entry in nil map

// 追加・更新
ages["bob"] = 25

// 存在チェック
v, ok := ages["carol"] // v=0, ok=false
if _, ok := ages["alice"]; ok {}

// 削除
delete(ages, "bob")

// 走査(順序はランダム。毎回変わる)
for k, v := range ages {}

// 順序が必要ならキーをソート
keys := slices.Sorted(maps.Keys(ages)) // Go 1.23

マップは 参照型(正確にはポインタを持つ構造体)なので、関数に渡しても中身の変更が反映されます。

構造体をマップの値にした場合、フィールドを直接変更できません。

type P struct{ X int }
m := map[string]P{"a": {1}}
// m["a"].X = 2  → コンパイルエラー

p := m["a"]
p.X = 2
m["a"] = p       // 取り出して変更して戻す

// または値をポインタにする
m2 := map[string]*P{"a": {1}}
m2["a"].X = 2    // OK

7. 文字列と rune

Go の string は UTF-8 のバイト列 です。

s := "日本語"
fmt.Println(len(s))         // 9(バイト数)
fmt.Println(len([]rune(s))) // 3(文字数)

// バイト単位のインデックス
fmt.Println(s[0])           // 230(1 バイト目)

// 文字単位で走査するには range
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r) // 0: 日, 3: 本, 6: 語
}

// utf8 パッケージ
import "unicode/utf8"
utf8.RuneCountInString(s)   // 3

文字列は 不変 です。連結を繰り返すなら strings.Builder を使います。

var sb strings.Builder
for i := 0; i < 1000; i++ {
    sb.WriteString("x")
}
result := sb.String()

よく使う strings / strconv

strings.Contains("hello", "ell")     // true
strings.Split("a,b,c", ",")          // [a b c]
strings.Join([]string{"a","b"}, "-") // a-b
strings.TrimSpace("  hi  ")          // hi
strings.ToUpper("go")                // GO
strings.HasPrefix("golang", "go")    // true
strings.ReplaceAll("aaa", "a", "b")  // bbb
strings.Fields("a  b   c")           // [a b c](空白で分割)

n, err := strconv.Atoi("42")         // string → int
s := strconv.Itoa(42)                // int → string
f, err := strconv.ParseFloat("3.14", 64)
b, err := strconv.ParseBool("true")

fmt の書式

fmt.Printf("%d %s %f %t\n", 1, "a", 1.5, true)
fmt.Printf("%v\n", anything)  // デフォルト表示
fmt.Printf("%+v\n", struct_)  // フィールド名つき
fmt.Printf("%#v\n", struct_)  // Go の構文として
fmt.Printf("%T\n", x)         // 型名
fmt.Printf("%q\n", "str")     // "str"(クォートつき)
fmt.Printf("%p\n", &x)        // ポインタ
fmt.Printf("%5d|%-5d|%05d\n", 42, 42, 42) // "   42|42   |00042"
fmt.Printf("%.2f\n", 3.14159) // 3.14

s := fmt.Sprintf("%d-%d", 1, 2) // 文字列として取得

8. 構造体とメソッド

構造体の定義と初期化

type User struct {
    ID        int64
    Name      string
    Email     string
    CreatedAt time.Time
}

// フィールド名指定(推奨。順序変更に強い)
u := User{ID: 1, Name: "Alice"}

// 順序指定(非推奨。フィールド追加で壊れる)
u2 := User{1, "Bob", "bob@example.com", time.Now()}

// ポインタで生成
u3 := &User{Name: "Carol"}

// ゼロ値
var u4 User

メソッド

構造体(および任意の型定義)に関数を紐づけられます。

// 値レシーバ:u はコピー。呼び出し元に影響しない
func (u User) DisplayName() string {
    return fmt.Sprintf("%s <%s>", u.Name, u.Email)
}

// ポインタレシーバ:元の値を変更できる
func (u *User) Rename(name string) {
    u.Name = name
}

u := User{Name: "Alice"}
u.Rename("Alicia")  // Go が自動で &u にしてくれる

レシーバの選び方

状況レシーバ
フィールドを変更するポインタ
構造体が大きい(数十バイト以上)ポインタ
sync.Mutex などコピー不可なフィールドを含むポインタ
小さくて不変(time.Time のような値型として扱いたい)値
迷ったらポインタ

1 つの型のメソッドは、値レシーバとポインタレシーバを混在させない のが慣習です。

コンストラクタ

Go にコンストラクタ構文はありません。New〜 という関数を書きます。

func NewUser(name, email string) (*User, error) {
    if name == "" {
        return nil, errors.New("name is required")
    }
    return &User{
        Name:      name,
        Email:     email,
        CreatedAt: time.Now(),
    }, nil
}

パッケージ名と重複する場合は省略します(user.New() のように)。

埋め込み(合成)

継承の代わりに 埋め込み で機能を合成します。

type Animal struct {
    Name string
}

func (a Animal) Speak() string { return a.Name + " makes a sound" }

type Dog struct {
    Animal        // 型名だけ書く = 埋め込み
    Breed string
}

d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Shiba"}
fmt.Println(d.Name)    // フィールドが昇格
fmt.Println(d.Speak()) // メソッドも昇格

// 上書き(オーバーライド風)
func (d Dog) Speak() string { return d.Name + " barks" }

実務では sync.Mutex の埋め込みや、http.Handler を包む場面でよく使います。

構造体タグ

フィールドにメタデータを付けます。JSON・DB・バリデーションで使われます。

type User struct {
    ID    int64  `json:"id" db:"id"`
    Name  string `json:"name" validate:"required"`
    Email string `json:"email,omitempty"`  // 空なら出力しない
    pass  string `json:"-"`                // 常に出力しない
}

構造体の比較

すべてのフィールドが比較可能なら == で比較できます。スライス・マップ・関数を含むと不可です。

type Point struct{ X, Y int }
Point{1, 2} == Point{1, 2} // true

// マップのキーにも使える
visited := map[Point]bool{}

9. ポインタ

x := 10
p := &x        // *int 型。x のアドレス
fmt.Println(*p) // 10(デリファレンス)
*p = 20
fmt.Println(x)  // 20

値渡しとポインタ渡し

Go は すべて値渡し です。構造体を渡すとコピーされます。

type Counter struct{ N int }

func incByValue(c Counter)  { c.N++ }   // コピーを変更。無意味
func incByPtr(c *Counter)   { c.N++ }   // 元を変更

c := Counter{}
incByValue(c)
fmt.Println(c.N) // 0
incByPtr(&c)
fmt.Println(c.N) // 1

new と &T{}

p1 := new(int)      // *int。ゼロ値へのポインタ
p2 := &User{}       // *User。こちらが一般的

nil ポインタ

nil ポインタをデリファレンスすると panic します。

var u *User
// fmt.Println(u.Name) → panic: nil pointer dereference

if u != nil {
    fmt.Println(u.Name)
}

ただし nil ポインタに対するメソッド呼び出しは可能 で、メソッド内で nil チェックすれば動きます(nil レシーバ)。

ポインタ演算はない

C のような p++ はできません。安全性のためです。


10. インターフェース

Go の設計思想の中心です。ここは時間をかけてください。

暗黙的な実装

type Speaker interface {
    Speak() string
}

type Dog struct{}
type Cat struct{}

func (Dog) Speak() string { return "Woof" }
func (Cat) Speak() string { return "Meow" }

// Dog も Cat も「implements」を書かずに Speaker を満たす
func talk(s Speaker) {
    fmt.Println(s.Speak())
}

talk(Dog{})
talk(Cat{})

メソッドの集合が一致すれば、その型はインターフェースを満たします。インターフェースの定義側と実装側が互いを知らなくてよい のが強みです。

メソッドセットとポインタレシーバ

ポインタレシーバのメソッドは ポインタ型のメソッドセットにだけ 含まれます。

type Writer interface{ Write(s string) }

type File struct{ data string }

func (f *File) Write(s string) { f.data += s }

var w Writer
w = &File{}   // OK
// w = File{}  → コンパイルエラー: File does not implement Writer

空インターフェースと any

var x any = 42       // any は interface{} の別名
x = "string"
x = []int{1}

// 型アサーション
s, ok := x.(string)  // ok=false なら s はゼロ値
n := x.(int)         // ok を省くと失敗時に panic

// 型スイッチ
switch v := x.(type) {
case int:
    fmt.Println("int", v)
case string:
    fmt.Println("string", v)
case nil:
    fmt.Println("nil")
default:
    fmt.Printf("unknown %T\n", v)
}

標準ライブラリの小さなインターフェース

// io パッケージ
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}

// fmt.Stringer:Println で表示をカスタマイズ
type Stringer interface {
    String() string
}

// error
type error interface {
    Error() string
}

io.Reader を受け取る関数は、ファイル・ネットワーク・文字列・HTTP ボディなど 何でも受け取れます。

func countLines(r io.Reader) int {
    sc := bufio.NewScanner(r)
    n := 0
    for sc.Scan() {
        n++
    }
    return n
}

countLines(os.Stdin)
countLines(strings.NewReader("a\nb\nc"))
f, _ := os.Open("file.txt")
countLines(f)

インターフェースの合成

type ReadWriter interface {
    Reader
    Writer
}

設計の指針

  1. 小さく作る:1〜3 メソッドが理想。io.Reader は 1 つ
  2. 使う側で定義する:「私はこのメソッドだけ必要」と受け手が宣言する
  3. 実装側で先に定義しない:必要になってから抽象化する
  4. コンパイル時チェック:var _ Speaker = (*Dog)(nil) と書くと実装漏れを検出できる

インターフェースの nil

インターフェース値は「型」と「値」のペアです。型が入っていると、値が nil でもインターフェース全体は nil ではありません。

func mayFail() error {
    var e *MyError = nil
    return e // 型 *MyError、値 nil → non-nil な error!
}

if err := mayFail(); err != nil {
    fmt.Println("エラー扱いになる") // ここに来る
}

エラーを返す関数では、成功時は必ず リテラルの nil を返します。


11. エラー処理

Go には例外がなく、エラーは値です。この節はバックエンド開発で毎日使います。

基本パターン

f, err := os.Open("config.yaml")
if err != nil {
    return err
}
defer f.Close()

エラーを無視しない(_ で捨てない)、すぐに処理する(後回しにしない)が原則です。

エラーの生成

// 単純なメッセージ
errors.New("something went wrong")

// 書式付き
fmt.Errorf("user %d not found", id)

// 既存エラーをラップ(%w が重要)
fmt.Errorf("load config: %w", err)

ラップと Is / As

%w でラップすると、呼び出し元が 元のエラーを判定 できます。

var ErrNotFound = errors.New("not found")   // センチネルエラー

func findUser(id int) error {
    return fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
}

err := findUser(1)
errors.Is(err, ErrNotFound)  // true(ラップの連鎖を辿る)
err == ErrNotFound           // false(直接比較は失敗する)

独自エラー型は errors.As で取り出します。

type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return e.Field + ": " + e.Msg
}

func validate(name string) error {
    if name == "" {
        return &ValidationError{Field: "name", Msg: "required"}
    }
    return nil
}

err := fmt.Errorf("signup: %w", validate(""))

var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println(ve.Field) // name
}

エラーメッセージの書き方

  • 小文字で始め、句点で終わらない(連結されるため)
  • 「何をしようとして失敗したか」を書く
  • 呼び出し元がすでに知っている情報を繰り返さない
// ✗
return fmt.Errorf("Error: Failed to open file.")

// ✓
return fmt.Errorf("open config %q: %w", path, err)
// 結果: "open config "app.yaml": no such file or directory"

複数エラーの結合(Go 1.20+)

err := errors.Join(err1, err2)
errors.Is(err, err1) // true

エラー処理の判断フロー

  1. 自分で対処できる → 対処して nil を返す
  2. 対処できないが文脈を足せる → fmt.Errorf("...: %w", err) で返す
  3. 呼び出し元が種類で分岐する必要がある → センチネル or 独自型を返す
  4. プログラムのバグ(到達不能な状態) → panic

12. defer・panic・recover

defer

関数が どの経路で終わっても 実行される後処理を登録します。リソース解放の定番です。

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // return の直前に実行される

    // ... f を使う。途中で return しても Close される
    return nil
}
  • 複数の defer は LIFO(後に登録したものから)実行
  • defer の引数は 登録時に評価 される
for i := 0; i < 3; i++ {
    defer fmt.Println(i) // 2 1 0 の順で出力
}

x := 1
defer fmt.Println(x) // 1 が出力される(後で x=2 にしても)
x = 2

ループ内で defer すると関数終了まで溜まり続けるので、ループ本体を関数に切り出します。

panic と recover

panic はプログラムを異常終了させます。バグ検出用 であり、通常のエラー処理には使いません。

func mustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("invalid config value %q", s))
    }
    return n
}

recover は defer 内で panic を捕まえます。HTTP サーバーが 1 リクエストの panic で全体を落とさないために使います(第3回のミドルウェアで登場)。

func safeCall(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    f()
    return nil
}

13. クロージャ

関数リテラルは外側の変数を 参照で 捕捉します。

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

next := counter()
next() // 1
next() // 2

ミドルウェアやハンドラ生成でよく使います。

func withPrefix(prefix string) func(string) string {
    return func(s string) string { return prefix + s }
}

ループ変数の捕捉(Go 1.22 で修正)

Go 1.21 以前はループ変数が共有されるバグの温床でしたが、Go 1.22 から各イテレーションで新しい変数 になりました。go.mod の go ディレクティブが 1.22 以上なら心配不要です。


14. ジェネリクス

Go 1.18 で導入されました。型パラメータ で汎用関数・型を書けます。

// [T any] が型パラメータ
func Map[T, U any](xs []T, f func(T) U) []U {
    out := make([]U, 0, len(xs))
    for _, x := range xs {
        out = append(out, f(x))
    }
    return out
}

func Filter[T any](xs []T, pred func(T) bool) []T {
    var out []T
    for _, x := range xs {
        if pred(x) {
            out = append(out, x)
        }
    }
    return out
}

nums := []int{1, 2, 3, 4}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
strs := Map(evens, strconv.Itoa)

制約(constraints)

import "cmp"

// cmp.Ordered = 比較演算子が使える型
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// 独自制約
type Number interface {
    ~int | ~int64 | ~float64  // ~ は「基底型がこれ」の意味
}

func Sum[T Number](xs []T) T {
    var total T
    for _, x := range xs {
        total += x
    }
    return total
}

ジェネリック型

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    v := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return v, true
}

s := &Stack[string]{}
s.Push("a")

使いすぎない

Go のジェネリクスはコレクション操作や汎用データ構造のためのものです。ビジネスロジックにジェネリクスが出てきたら、まず設計を疑ってください。多くの場合、インターフェースの方が読みやすくなります。


15. goroutine と channel(入門)

第5回で深く扱うので、ここでは概念だけ。

goroutine

go キーワードで関数を 並行実行 します。OS スレッドより軽量で、数万個作れます。

func say(s string) {
    for i := 0; i < 3; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
    // main が終わると goroutine も終わる。"world" が全部出るとは限らない
}

channel

goroutine 間で値を 安全に受け渡す パイプです。

func main() {
    ch := make(chan int)

    go func() {
        ch <- 42 // 送信(受信されるまでブロック)
    }()

    v := <-ch // 受信(送信されるまでブロック)
    fmt.Println(v)
}

sync.WaitGroup

複数の goroutine の完了を待ちます。

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}
wg.Wait()

共有変数を複数 goroutine から同時に書き換えるとデータ競合 になります。第5回で sync.Mutex や -race フラグとともに詳しく解説します。


16. よくある落とし穴

シャドーイング

err := doA()
if err != nil {
    return err
}
if true {
    err := doB() // 新しい err!外側の err は更新されない
    _ = err
}

go vet -vettool や golangci-lint の govet で検出できます。

スライスの共有

6 章で説明した通り。append の戻り値を必ず受け取り、部分スライスを他に渡すときは slices.Clone を検討します。

マップへの並行書き込み

複数 goroutine からマップに書くと fatal error: concurrent map writes で落ちます。sync.Mutex か sync.Map で守ります。

nil マップへの書き込み

var m map[string]int のあと m["a"]=1 は panic。make か リテラルで初期化します。

未使用変数・import

コンパイルエラーになります。一時的に黙らせるなら _ = x。

:= による意図しない新規宣言

x, err := f()
y, err := g()  // err は再代入(x,err のうち y が新規なので OK)

var z int
z, err := h()  // コンパイルエラー: no new variables(同スコープで両方既存)

構造体を値でループすると変更が反映されない

for _, u := range users {
    u.Name = "x" // コピーを変更している
}

for i := range users {
    users[i].Name = "x" // OK
}

time.Duration の単位

time.Sleep(100)                       // 100 ナノ秒!
time.Sleep(100 * time.Millisecond)    // 正しい

17. 開発ツール

gofmt -w .              # 整形(エディタが自動で実行)
goimports -w .          # import も整理
go vet ./...            # 静的解析(怪しいコードを検出)
go build ./...          # 全パッケージをビルド
go test ./...           # 全テスト
go doc fmt.Println      # ドキュメント表示
go env                  # 環境変数
go mod tidy             # 依存整理
go list -m all          # 依存一覧

より強力な lint は golangci-lint:

brew install golangci-lint
golangci-lint run ./...

ドキュメントは pkg.go.dev で検索します。標準ライブラリのソースは読みやすいので、fmt.Println の実装を追ってみるのも勉強になります。


18. 演習問題

各問題を解いたら go vet と gofmt を通してください。

基礎

  1. FizzBuzz:1〜100 で 3 の倍数は Fizz、5 の倍数は Buzz、両方は FizzBuzz を出力する
  2. 単語カウント:文字列を受け取り、各単語の出現回数を map[string]int で返す関数 WordCount(s string) map[string]int を書く。キーをソートして表示する
  3. 温度変換:type Celsius float64 と type Fahrenheit float64 を定義し、相互変換する関数と String() メソッドを実装する

構造体・インターフェース

  1. 図形:Shape インターフェース(Area(), Perimeter())を定義し、Circle、Rectangle、Triangle で実装する。[]Shape を受け取って合計面積を返す関数を書く
  2. スタック:ジェネリックな Stack[T] を実装し、Push、Pop、Peek、Len を持たせる。Pop は空のとき error を返す

エラー処理

  1. 銀行口座:Account 構造体に Deposit、Withdraw メソッドを実装する。残高不足のときは独自エラー型 InsufficientFundsError(不足額を持つ)を返す。呼び出し側で errors.As を使って不足額を表示する
  2. 設定パーサー:"key=value" 形式の行を複数受け取り、map[string]string に変換する。不正な行があれば行番号を含むエラーを返す(%w でラップし、センチネルエラー ErrInvalidLine を判定できるようにする)

応用

  1. 文字列処理:日本語を含む文字列を受け取り、文字数(rune 数)、バイト数、大文字化した結果を返す
  2. クロージャ:呼ばれた回数と最後に呼ばれた時刻を記録する関数を返す Tracker() を書く
  3. 簡易 io.Reader:文字列を大文字に変換しながら読み出す UpperReader 型を作り、io.Reader を満たすようにする。io.Copy(os.Stdout, &UpperReader{...}) で動作確認する

次回予告

第2回では、これらの文法を使って JSON ファイルにデータを保存する Todo CLI を作りながら、モジュール管理・ファイル I/O・テストの書き方を学びます。

B!
← 一覧へ戻る