シリーズ構成
- Go の基本文法(本記事)
- CLI ツールを作る
- HTTP サーバーと REST API
- データベース連携
- 実務的な周辺技術(Docker・設定・ログ・認証・並行処理)
- ポートフォリオを作る
- 実践知識を深める
この回の目標は 「Go の文法要素をすべて触り、他人が書いた Go のコードを詰まらずに読める状態になる」 ことです。分量は多いですが、すべて手で打ち込んで実行してください。写経の速度より、ひとつひとつの挙動を確かめることを優先します。
目次
- 環境構築と最初のプログラム
- パッケージとモジュール
- 変数・定数・型
- 関数
- 制御構文
- 配列・スライス・マップ
- 文字列と rune
- 構造体とメソッド
- ポインタ
- インターフェース
- エラー処理
- defer・panic・recover
- クロージャ
- ジェネリクス
- goroutine と channel(入門)
- よくある落とし穴
- 開発ツール
- 演習問題
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 | "" |
bool | false |
| ポインタ、スライス、マップ、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〜3 メソッドが理想。
io.Readerは 1 つ - 使う側で定義する:「私はこのメソッドだけ必要」と受け手が宣言する
- 実装側で先に定義しない:必要になってから抽象化する
- コンパイル時チェック:
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
エラー処理の判断フロー
- 自分で対処できる → 対処して
nilを返す - 対処できないが文脈を足せる →
fmt.Errorf("...: %w", err)で返す - 呼び出し元が種類で分岐する必要がある → センチネル or 独自型を返す
- プログラムのバグ(到達不能な状態) →
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 を通してください。
基礎
- FizzBuzz:1〜100 で 3 の倍数は Fizz、5 の倍数は Buzz、両方は FizzBuzz を出力する
- 単語カウント:文字列を受け取り、各単語の出現回数を
map[string]intで返す関数WordCount(s string) map[string]intを書く。キーをソートして表示する - 温度変換:
type Celsius float64とtype Fahrenheit float64を定義し、相互変換する関数とString()メソッドを実装する
構造体・インターフェース
- 図形:
Shapeインターフェース(Area(),Perimeter())を定義し、Circle、Rectangle、Triangleで実装する。[]Shapeを受け取って合計面積を返す関数を書く - スタック:ジェネリックな
Stack[T]を実装し、Push、Pop、Peek、Lenを持たせる。Popは空のときerrorを返す
エラー処理
- 銀行口座:
Account構造体にDeposit、Withdrawメソッドを実装する。残高不足のときは独自エラー型InsufficientFundsError(不足額を持つ)を返す。呼び出し側でerrors.Asを使って不足額を表示する - 設定パーサー:
"key=value"形式の行を複数受け取り、map[string]stringに変換する。不正な行があれば行番号を含むエラーを返す(%wでラップし、センチネルエラーErrInvalidLineを判定できるようにする)
応用
- 文字列処理:日本語を含む文字列を受け取り、文字数(rune 数)、バイト数、大文字化した結果を返す
- クロージャ:呼ばれた回数と最後に呼ばれた時刻を記録する関数を返す
Tracker()を書く - 簡易 io.Reader:文字列を大文字に変換しながら読み出す
UpperReader型を作り、io.Readerを満たすようにする。io.Copy(os.Stdout, &UpperReader{...})で動作確認する
次回予告
第2回では、これらの文法を使って JSON ファイルにデータを保存する Todo CLI を作りながら、モジュール管理・ファイル I/O・テストの書き方を学びます。
Analyzegear