Day 1: Go言語の核心 - 講義
今日の目標
- Go言語の設計思想と哲学を理解する
- 他言語との違いを明確にする
- Goの型システムと暗黙の変換の非存在を理解する
- ゼロ値の概念とその重要性を学ぶ
- ポインタと値の違いを完全に理解する
---
Go言語とは何か
設計思想
Go言語は2009年にGoogle社が開発した、シンプルさと効率性を追求したプログラミング言語です。
主要な設計原則:
- シンプルさ: 複雑な機能を削ぎ落とし、理解しやすい言語仕様
- 並行性: ゴルーチンとチャネルによる軽量な並行処理
- 高速なコンパイル: 大規模プロジェクトでも数秒でビルド
- 静的型付け: コンパイル時の型安全性
- ガベージコレクション: メモリ管理の自動化
他言語との比較
| 特徴 | Go | C/C++ | Java | Python | Rust |
|---|---|---|---|---|---|
| コンパイル速度 | ◎ | △ | ○ | - | △ |
| 実行速度 | ◎ | ◎ | ○ | △ | ◎ |
| メモリ安全性 | ○ | × | ◎ | ◎ | ◎ |
| 並行処理 | ◎ | △ | ○ | △ | ◎ |
| 学習コスト | ○ | × | △ | ◎ | × |
Goが「持たない」もの
Goは意図的に以下の機能を持ちません:
// クラス → 構造体とメソッドで代替
// 継承 → 埋め込みとインターフェースで代替
// ジェネリクス → Go 1.18から追加(限定的)
// 例外 → 明示的なエラー処理
// アノテーション/デコレータ → なし
// マクロ → なし
// デフォルト引数 → なし
// 演算子オーバーロード → なし
これらは「不要」ではなく、シンプルさを保つための選択です。
---
型システム
静的型付けと型推論
package main
import "fmt"
func main() {
// 明示的な型指定
var age int = 25
var name string = "太郎"
// 型推論(:=)
age2 := 25 // int型と推論
name2 := "太郎" // string型と推論
// 型推論後は型が固定
age2 = 30 // OK
// age2 = "30" // コンパイルエラー
fmt.Println(age, name, age2, name2)
}
暗黙の型変換の非存在
重要: Goは暗黙の型変換を一切行いません。
package main
import "fmt"
func main() {
var i int = 10
var f float64 = 3.14
// これはエラー
// result := i + f // コンパイルエラー
// 明示的な変換が必要
result := float64(i) + f
fmt.Println(result)
// intとint64も別の型
var a int = 10
var b int64 = 20
// c := a + b // エラー
c := int64(a) + b
fmt.Println(c)
}
基本型の詳細
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
// 整数型のサイズ
var i8 int8 // -128 to 127
var i16 int16 // -32768 to 32767
var i32 int32 // -2^31 to 2^31-1
var i64 int64 // -2^63 to 2^63-1
// 符号なし整数
var u8 uint8 // 0 to 255
var u16 uint16 // 0 to 65535
var u32 uint32 // 0 to 2^32-1
var u64 uint64 // 0 to 2^64-1
// プラットフォーム依存の整数(32bit or 64bit)
var i int
var u uint
// 浮動小数点
var f32 float32 // IEEE-754 32-bit
var f64 float64 // IEEE-754 64-bit
// 複素数
var c64 complex64
var c128 complex128
// 文字列(UTF-8エンコード、イミュータブル)
var s string = "Hello, 世界"
// rune(int32のエイリアス、Unicodeコードポイント)
var r rune = '世'
// byte(uint8のエイリアス)
var b byte = 255
fmt.Printf("int size: %d bytes\n", unsafe.Sizeof(i))
fmt.Printf("string: %s, type: %v\n", s, reflect.TypeOf(s))
fmt.Printf("rune: %c, value: %d\n", r, r)
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ = i8, i16, i32, i64, u8, u16, u32, u64, i, u, f32, f64, c64, c128, r, b
}
---
ゼロ値の概念
Go言語の最も重要な特徴の1つ: すべての変数は宣言時にゼロ値で初期化されます。
package main
import "fmt"
func main() {
// ゼロ値
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // ""(空文字列)
var p *int // nil
var slice []int // nil
var m map[string]int // nil
var ch chan int // nil
fmt.Printf("int: %v\n", i)
fmt.Printf("float64: %v\n", f)
fmt.Printf("bool: %v\n", b)
fmt.Printf("string: %q\n", s)
fmt.Printf("pointer: %v\n", p)
fmt.Printf("slice: %v\n", slice)
fmt.Printf("map: %v\n", m)
fmt.Printf("chan: %v\n", ch)
// ゼロ値は有効な値として使える
if i == 0 {
fmt.Println("iはゼロ値")
}
// しかし、nilのマップとチャネルは使用不可
// m["key"] = 1 // パニック
m = make(map[string]int) // 初期化が必要
m["key"] = 1
}
ゼロ値の利点:
- 未初期化変数によるバグを防ぐ
- 明示的な初期化が不要な場合が多い
- 構造体のフィールドが自動的に初期化される
構造体とゼロ値
package main
import "fmt"
type Config struct {
Host string
Port int
Timeout int
Debug bool
}
func main() {
// すべてのフィールドがゼロ値で初期化される
var config Config
fmt.Printf("%+v\n", config)
// {Host: Port:0 Timeout:0 Debug:false}
// 一部だけ指定
config2 := Config{
Host: "localhost",
Port: 8080,
// TimeoutとDebugはゼロ値
}
fmt.Printf("%+v\n", config2)
// {Host:localhost Port:8080 Timeout:0 Debug:false}
// ゼロ値を利用したデフォルト値パターン
if config2.Timeout == 0 {
config2.Timeout = 30 // デフォルト30秒
}
}
---
ポインタと値
ポインタの基本
package main
import "fmt"
func main() {
x := 42
// ポインタの取得(&)
p := &x
fmt.Printf("xの値: %d\n", x)
fmt.Printf("xのアドレス: %p\n", p)
// デリファレンス(*)
fmt.Printf("ポインタが指す値: %d\n", *p)
// ポインタ経由で値を変更
*p = 100
fmt.Printf("変更後のx: %d\n", x)
}
値渡しとポインタ渡し
package main
import "fmt"
func modifyValue(x int) {
x = 100
}
func modifyPointer(x *int) {
*x = 100
}
func main() {
a := 42
b := 42
modifyValue(a)
fmt.Println("値渡し後:", a) // 42(変更されない)
modifyPointer(&b)
fmt.Println("ポインタ渡し後:", b) // 100(変更される)
}
構造体とポインタ
package main
import "fmt"
type Person struct {
Name string
Age int
}
func modifyValue(p Person) {
p.Age = 100
}
func modifyPointer(p *Person) {
p.Age = 100
// (*p).Age = 100 // これでもOKだが、Goは自動でデリファレンスしてくれる
}
func main() {
person1 := Person{Name: "太郎", Age: 25}
person2 := Person{Name: "花子", Age: 22}
modifyValue(person1)
fmt.Printf("person1: %+v\n", person1) // Age: 25
modifyPointer(&person2)
fmt.Printf("person2: %+v\n", person2) // Age: 100
}
newとmake
package main
import "fmt"
func main() {
// new: ゼロ値で初期化し、ポインタを返す
p := new(int)
fmt.Printf("型: %T, 値: %v, ポインタが指す値: %v\n", p, p, *p)
// 型: *int, 値: 0xc000012098, ポインタが指す値: 0
// 構造体の場合
type Person struct {
Name string
Age int
}
person := new(Person)
fmt.Printf("%+v\n", person) // &{Name: Age:0}
person.Name = "太郎"
fmt.Printf("%+v\n", person) // &{Name:太郎 Age:0}
// make: スライス、マップ、チャネルの初期化専用
slice := make([]int, 5) // 長さ5のスライス
m := make(map[string]int) // マップ
ch := make(chan int) // チャネル
fmt.Printf("slice: %v\n", slice)
fmt.Printf("map: %v\n", m)
fmt.Printf("chan: %v\n", ch)
}
newとmakeの違い:
new(T): 任意の型Tでゼロ値を割り当て、Tを返すmake(T, args): スライス、マップ、チャネル専用。初期化済みのTを返す
---
スライスの内部構造
スライスは参照型
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 999
}
func main() {
original := []int{1, 2, 3, 4, 5}
fmt.Println("変更前:", original)
modifySlice(original)
fmt.Println("変更後:", original) // [999 2 3 4 5]
// スライスは内部で配列へのポインタを持つ
}
スライスの構造
スライスは以下の3つの要素で構成されます:
type slice struct {
ptr *array // 内部配列へのポインタ
len int // 長さ
cap int // 容量
}
package main
import "fmt"
func main() {
s := make([]int, 5, 10) // 長さ5、容量10
fmt.Printf("長さ: %d, 容量: %d\n", len(s), cap(s))
// 容量内なら再割り当てなし
s = append(s, 1, 2, 3, 4, 5)
fmt.Printf("append後 - 長さ: %d, 容量: %d\n", len(s), cap(s))
// 容量を超えると新しい配列が割り当てられる
s = append(s, 6)
fmt.Printf("容量超過後 - 長さ: %d, 容量: %d\n", len(s), cap(s))
}
スライスの落とし穴
package main
import "fmt"
func main() {
// 問題1: 部分スライスは元の配列を共有
original := []int{1, 2, 3, 4, 5}
sub := original[1:4]
sub[0] = 999
fmt.Println("original:", original) // [1 999 3 4 5]
fmt.Println("sub:", sub) // [999 3 4]
// 解決策: コピーを作る
original2 := []int{1, 2, 3, 4, 5}
sub2 := make([]int, 3)
copy(sub2, original2[1:4])
sub2[0] = 999
fmt.Println("original2:", original2) // [1 2 3 4 5]
fmt.Println("sub2:", sub2) // [999 3 4]
}
---
マップの詳細
マップの内部実装
マップはハッシュテーブルとして実装されています。
package main
import "fmt"
func main() {
// nilマップ
var m1 map[string]int
fmt.Println("m1 == nil:", m1 == nil) // true
// nilマップは読み取りOK、書き込みNG
fmt.Println("m1['key']:", m1["key"]) // 0(ゼロ値)
// m1["key"] = 1 // パニック
// makeで初期化
m2 := make(map[string]int)
m2["key"] = 1 // OK
// 容量のヒント(最適化)
m3 := make(map[string]int, 100) // 100要素分を事前確保
_ = m3
}
マップの安全な使用
package main
import "fmt"
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
// 値と存在確認
value, exists := m["a"]
fmt.Printf("a: value=%d, exists=%v\n", value, exists)
value, exists = m["c"]
fmt.Printf("c: value=%d, exists=%v\n", value, exists)
// 削除
delete(m, "a")
fmt.Println("削除後:", m)
// 削除済みを再度削除してもエラーにならない
delete(m, "a")
}
---
エラー処理の哲学
エラーは値
Goは例外を投げません。代わりに、エラーを値として返します。
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("ゼロ除算エラー")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
result, err = divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
}
早期リターンパターン
package main
import (
"fmt"
"os"
)
func processFile(filename string) error {
// エラーがあればすぐに返す
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// ... 処理 ...
return nil
}
func main() {
if err := processFile("test.txt"); err != nil {
fmt.Println("エラー:", err)
}
}
---
Go言語の歴史と設計思想の深掘り
Goの誕生背景
Go言語は2007年、Google社のRobert Griesemer、Rob Pike、Ken Thompsonによって設計が開始されました。2009年11月10日にオープンソースプロジェクトとして公開され、2012年3月にバージョン1.0がリリースされました。
開発の動機:
- コンパイル時間の長さ: C++の大規模プロジェクトでは、フルビルドに数時間かかることも
- 言語の複雑さ: C++やJavaは機能が多すぎて、チーム全体が言語仕様を理解するのが困難
- 並行処理の難しさ: マルチコアCPUが普及したが、既存言語では並行処理が複雑
- 依存関係の管理: ヘッダーファイルの循環依存など、管理が困難
- ガベージコレクションの欠如: C/C++では手動メモリ管理が必要
設計者の経歴と影響
Rob Pike:
- Plan 9、UTF-8の共同開発者
- UNIX開発チームのメンバー
- シンプルさと明確さを重視する設計哲学
Ken Thompson:
- UNIX、B言語、C言語の開発者
- チューリング賞受賞者
- 効率性とシステムプログラミングの専門家
Robert Griesemer:
- V8 JavaScriptエンジン、HotSpot JVMの開発者
- コンパイラ技術とガベージコレクションの専門家
- シングルバイナリ配布: 依存関係がないため、配布が容易
- クロスコンパイル: Linux、Windows、macOS向けに簡単にビルド可能
- 並行処理: コンテナの並列管理が効率的
- 低レイヤーアクセス: システムコールやcgroupsへの直接アクセス
この3人の経験が、Goの「シンプルで高速、かつ現代的」という特徴を形作りました。
---
実世界での採用事例
1. Docker(コンテナ技術)
Dockerは2013年にGoで書き直されました。当初Pythonで書かれていましたが、以下の理由でGoに移行:
// Dockerのコンテナ管理の簡略化例
type Container struct {
ID string
Image string
Status string
}
type Runtime struct {
containers map[string]*Container
mu sync.RWMutex
}
func (r *Runtime) Start(id string) error {
r.mu.Lock()
defer r.mu.Unlock()
container := r.containers[id]
// 並行処理でコンテナを起動
go container.Run()
return nil
}
2. Kubernetes(オーケストレーション)
Kubernetesは最初からGoで開発されました。
統計データ:
- コードベース: 約100万行のGo
- コントリビューター: 3,000人以上
- 企業採用率: Fortune 500企業の70%以上
- 高スループット: 毎秒数百万メトリクスを処理
- 効率的なメモリ管理: GCの最適化で低レイテンシを実現
- 並行クエリ: 複数のクエリを並行実行
3. Prometheus(モニタリング)
時系列データベースとモニタリングシステム。
// Prometheusのメトリクス収集の概念例
type Metric struct {
Name string
Value float64
Timestamp time.Time
Labels map[string]string
}
type Collector struct {
metrics chan Metric
}
func (c *Collector) Collect() {
// 並行してメトリクスを収集
var wg sync.WaitGroup
for _, target := range targets {
wg.Add(1)
go func(t Target) {
defer wg.Done()
metrics := t.Scrape()
for _, m := range metrics {
c.metrics <- m
}
}(target)
}
wg.Wait()
}
4. CockroachDB(分散データベース)
PostgreSQL互換の分散SQLデータベース。
5. Vitess(YouTube/Google)
MySQLのスケーリングを実現するデータベースクラスタリングシステム。
---
Go開発者の市場価値とキャリアパス
年収データ(2024-2025年)
日本市場:
- ジュニア(0-2年): 400万円〜600万円
- ミドル(3-5年): 600万円〜900万円
- シニア(5年以上): 900万円〜1,500万円
- テックリード/アーキテクト: 1,200万円〜2,000万円
米国市場:
- ジュニア: $80,000〜$120,000
- ミドル: $120,000〜$180,000
- シニア: $180,000〜$250,000
- スタッフエンジニア: $250,000〜$400,000+
リモートワーク(グローバル企業):
- ミドル: $100,000〜$150,000
- シニア: $150,000〜$250,000
求人需要のトレンド
増加している分野:
- クラウドインフラ: AWS、GCP、Azure向けのツール開発
- DevOps/SRE: インフラ自動化、モニタリング
- マイクロサービス: API Gateway、サービスメッシュ
- ブロックチェーン: Ethereum、Hyperledger
- エッジコンピューティング: IoT、CDN
人気企業での採用:
- Google: Kubernetes、Go言語本体
- Uber: マイクロサービス基盤
- Dropbox: ストレージシステム
- Netflix: インフラツール
- Cloudflare: エッジネットワーク
キャリアパスの例
レベル1(0-1年): Go基礎習得
├─ 基本的なCLIツール開発
├─ REST API実装
└─ ユニットテスト作成
レベル2(1-3年): 実践的開発
├─ マイクロサービス設計
├─ データベース最適化
├─ 並行処理パターン習得
└─ Dockerコンテナ化
レベル3(3-5年): アーキテクチャ設計
├─ システム全体の設計
├─ パフォーマンスチューニング
├─ セキュリティ実装
└─ チームリード
レベル4(5年以上): エキスパート
├─ 技術選定・意思決定
├─ 大規模システム設計
├─ OSS貢献・コミュニティ活動
└─ テックブログ執筆・講演
---
プロダクション環境での考慮事項
1. スケーラビリティパターン
垂直スケーリング(スケールアップ):
// GOMAXPROCS: 使用するCPUコア数の設定
runtime.GOMAXPROCS(runtime.NumCPU())
// ワーカープールパターン
type WorkerPool struct {
workers int
jobs chan Job
results chan Result
}
func NewWorkerPool(workers int) *WorkerPool {
wp := &WorkerPool{
workers: workers,
jobs: make(chan Job, 100),
results: make(chan Result, 100),
}
for i := 0; i < workers; i++ {
go wp.worker()
}
return wp
}
func (wp *WorkerPool) worker() {
for job := range wp.jobs {
result := job.Execute()
wp.results <- result
}
}
水平スケーリング(スケールアウト):
- ステートレス設計: セッション情報をRedisに保存
- 負荷分散: Nginx、HAProxy、Envoy
- サービス分割: マイクロサービスアーキテクチャ
2. 可観測性(Observability)
ログ:
import (
"go.uber.org/zap"
)
// 構造化ログ
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("user login",
zap.String("user_id", userID),
zap.String("ip", clientIP),
zap.Duration("duration", duration),
)
メトリクス:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "endpoint", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "endpoint"},
)
)
トレーシング:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
func processRequest(ctx context.Context, req Request) error {
tracer := otel.Tracer("myapp")
ctx, span := tracer.Start(ctx, "processRequest")
defer span.End()
// 処理の実装
span.SetAttributes(attribute.String("request.id", req.ID))
return nil
}
3. 障害耐性(Resilience)
サーキットブレーカー:
type CircuitBreaker struct {
maxFailures int
timeout time.Duration
mu sync.Mutex
failures int
lastFailTime time.Time
state string // "closed", "open", "half-open"
}
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == "open" {
if time.Since(cb.lastFailTime) > cb.timeout {
cb.state = "half-open"
} else {
return errors.New("circuit breaker is open")
}
}
err := fn()
if err != nil {
cb.failures++
cb.lastFailTime = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = "open"
}
return err
}
// 成功時はリセット
cb.failures = 0
cb.state = "closed"
return nil
}
リトライ戦略:
func RetryWithBackoff(fn func() error, maxRetries int) error {
var err error
backoff := time.Second
for i := 0; i < maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
if i < maxRetries-1 {
time.Sleep(backoff)
backoff *= 2 // エクスポネンシャルバックオフ
}
}
return fmt.Errorf("max retries exceeded: %w", err)
}
---
パフォーマンス最適化とプロファイリング
1. ベンチマーク測定
// ベンチマークの書き方
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "Hello, " + "World!"
}
}
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("Hello, ")
sb.WriteString("World!")
_ = sb.String()
}
}
// 実行
// go test -bench=. -benchmem
結果の読み方:
BenchmarkStringConcat-8 100000000 10.5 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 50000000 30.2 ns/op 24 B/op 2 allocs/op
ns/op: 1操作あたりのナノ秒B/op: 1操作あたりのバイト数allocs/op: 1操作あたりのメモリアロケーション数
2. CPU プロファイリング
import (
"os"
"runtime/pprof"
)
func main() {
// CPUプロファイリング開始
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// プログラム実行
doWork()
}
// 解析
// go tool pprof cpu.prof
// (pprof) top10
// (pprof) list functionName
3. メモリプロファイリング
import (
"os"
"runtime"
"runtime/pprof"
)
func main() {
doWork()
// メモリプロファイル取得
f, _ := os.Create("mem.prof")
runtime.GC() // GC実行
pprof.WriteHeapProfile(f)
f.Close()
}
// 解析
// go tool pprof mem.prof
// (pprof) top10
// (pprof) list functionName
4. Race Detector(データ競合検出)
// 競合が発生するコード例
var counter int
func increment() {
counter++ // データ競合!
}
func main() {
go increment()
go increment()
time.Sleep(time.Second)
}
// 検出
// go run -race main.go
// go test -race ./...
---
ソフトスキルと技術的コミュニケーション
1. 設計レビューの進め方
設計書のテンプレート:
# 機能設計書: ユーザー認証システム
## 目的
既存の認証システムをJWT方式に移行し、スケーラビリティを向上させる
## 背景
- 現状: セッションベース認証(メモリストア)
- 課題: 水平スケーリング時のセッション共有が困難
- 解決策: JWT + Redisによるトークン無効化管理
## 設計概要
### アーキテクチャ図
[図を挿入]
### コンポーネント
1. AuthService: JWT生成・検証
2. TokenStore: Redisベースの無効化リスト管理
3. Middleware: HTTPリクエストの認証チェック
### インターフェース定義
go
type AuthService interface {
GenerateToken(userID string) (string, error)
ValidateToken(token string) (Claims, error)
RevokeToken(token string) error
}
### データフロー
1. ユーザーログイン → JWT発行
2. リクエスト → JWT検証 → 無効化リスト確認
3. ログアウト → JWT無効化(Redisに登録)
## セキュリティ考慮事項
- トークン有効期限: 1時間
- リフレッシュトークン: 30日
- HTTPS必須
- CORS設定
## パフォーマンス目標
- トークン検証: <5ms (p99)
- Redis照会: <2ms (p99)
- スループット: 10,000 req/s
## マイグレーション計画
1. フェーズ1: 新システム並行稼働
2. フェーズ2: トラフィック段階的移行
3. フェーズ3: 旧システム廃止
## テスト計画
- ユニットテスト: 90%以上のカバレッジ
- 統合テスト: 主要フロー全カバー
- 負荷テスト: 10,000 req/s で安定稼働確認
## リスクと対策
| リスク | 影響 | 対策 |
|--------|------|------|
| Redis障害 | 認証不可 | フォールバック機構 |
| トークン漏洩 | 不正アクセス | 短い有効期限 |
2. RFC(Request for Comments)の書き方
社内RFCの例:
# RFC-001: エラーハンドリング標準化
## ステータス
提案中 / レビュー中 / 承認済み / 却下
## 概要
プロジェクト全体で一貫したエラーハンドリングパターンを導入する
## 動機
現状、各開発者が異なるエラー処理を実装しており、以下の問題が発生:
- ログの形式が不統一
- エラーの原因追跡が困難
- クライアントへのエラーレスポンスが一貫しない
## 提案
### エラー型の定義
go
type AppError struct {
Code string // エラーコード(例: "USER_NOT_FOUND")
Message string // ユーザー向けメッセージ
Err error // 元のエラー
Context map[string]interface{} // コンテキスト情報
}func (e AppError) Error() string { return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err) }
func (e AppError) Unwrap() error { return e.Err }
### 使用例
go
func GetUser(id string) (*User, error) {
user, err := db.Query(id)
if err == sql.ErrNoRows {
return nil, &AppError{
Code: "USER_NOT_FOUND",
Message: "ユーザーが見つかりません",
Err: err,
Context: map[string]interface{}{"user_id": id},
}
}
if err != nil {
return nil, &AppError{
Code: "DATABASE_ERROR",
Message: "データベースエラーが発生しました",
Err: err,
}
}
return user, nil
}
## 代替案
1. pkg/errorsパッケージのみ使用
2. サードパーティライブラリ(github.com/pkg/errors)
## 影響範囲
- 全サービスのエラーハンドリング修正必要
- APIレスポンス形式の変更
- ログフォーマットの統一
## 移行計画
1. 新規コードから適用開始
2. 既存コードは段階的に移行
3. 期限: 3ヶ月
## 未解決の課題
- パニック時の統一的なハンドリング方法
- エラーコードの命名規則
---
設計原則とアーキテクチャパターン
1. SOLID原則のGo実装
Single Responsibility Principle(単一責任の原則):
// 悪い例: 複数の責任を持つ
type UserService struct {
db *sql.DB
}
func (s *UserService) CreateUser(user *User) error {
// データベース操作
// メール送信
// ログ記録
// キャッシュ更新
}
// 良い例: 責任を分離
type UserRepository interface {
Save(user *User) error
}
type EmailService interface {
SendWelcomeEmail(email string) error
}
type UserService struct {
repo UserRepository
email EmailService
logger Logger
}
func (s *UserService) CreateUser(user *User) error {
if err := s.repo.Save(user); err != nil {
return err
}
if err := s.email.SendWelcomeEmail(user.Email); err != nil {
s.logger.Warn("failed to send email", err)
}
s.logger.Info("user created", user.ID)
return nil
}
Dependency Inversion Principle(依存性逆転の原則):
// 高レベルモジュールが低レベルモジュールに依存しない
// どちらも抽象(インターフェース)に依存する
// 抽象
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
// 高レベルモジュール
type Cache struct {
storage Storage // 抽象に依存
}
// 低レベルモジュール(実装)
type RedisStorage struct {
client *redis.Client
}
func (r *RedisStorage) Get(key string) ([]byte, error) {
return r.client.Get(context.Background(), key).Bytes()
}
func (r *RedisStorage) Set(key string, value []byte) error {
return r.client.Set(context.Background(), key, value, 0).Err()
}
// 使用
func main() {
redis := &RedisStorage{client: redis.NewClient(&redis.Options{})}
cache := &Cache{storage: redis}
}
2. クリーンアーキテクチャ
プロジェクト構造:
cmd/
app/
main.go # エントリーポイント
internal/
domain/ # ビジネスロジック(最内層)
user.go
repository.go # インターフェース定義
usecase/ # ユースケース層
user_interactor.go
interface/ # インターフェースアダプター層
repository/
user_repository.go
handler/
user_handler.go
infrastructure/ # 外部層
database/
postgres.go
http/
server.go
実装例:
// domain/user.go(ビジネスロジック)
type User struct {
ID string
Email string
Name string
}
type UserRepository interface {
FindByID(id string) (*User, error)
Save(user *User) error
}
// usecase/user_interactor.go(ユースケース)
type UserInteractor struct {
repo UserRepository
}
func (i *UserInteractor) GetUser(id string) (*User, error) {
return i.repo.FindByID(id)
}
// interface/repository/user_repository.go(実装)
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) FindByID(id string) (*User, error) {
// SQL実行
}
// interface/handler/user_handler.go(HTTPハンドラー)
type UserHandler struct {
interactor *UserInteractor
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
user, err := h.interactor.GetUser(id)
// レスポンス返却
}
---
Go固有のイディオムとベストプラクティス
1. Accept Interfaces, Return Structs
// 良い例
func ProcessData(r io.Reader) (*Result, error) {
// インターフェースを受け取る → 柔軟性
// 具象型を返す → シンプル
}
// 悪い例
func ProcessData(r *os.File) (io.Reader, error) {
// 具象型を受け取る → 制限的
// インターフェースを返す → 複雑
}
2. エラーラッピング(Go 1.13+)
import "fmt"
func readConfig(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// ...
return nil
}
func main() {
err := readConfig("config.yaml")
if err != nil {
// エラーチェーン全体を表示
fmt.Printf("%+v\n", err)
// 特定のエラーを判定
if errors.Is(err, os.ErrNotExist) {
fmt.Println("設定ファイルが存在しません")
}
}
}
3. Functional Options Pattern(上級)
type Server struct {
host string
port int
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(opts ...Option) *Server {
// デフォルト値
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
logger: log.Default(),
}
// オプション適用
for _, opt := range opts {
opt(s)
}
return s
}
// 使用
func main() {
server := NewServer(
WithHost("0.0.0.0"),
WithPort(3000),
WithTimeout(60 * time.Second),
)
}
---
参考リソース
公式ドキュメント
書籍
- The Go Programming Language - Alan A. A. Donovan & Brian W. Kernighan
- Concurrency in Go - Katherine Cox-Buday
- Go in Action - William Kennedy
- Cloud Native Go - Matthew A. Titmus
オンラインリソース
コミュニティ
---
Go言語の核心を理解することで、なぜGoがこのように設計されているのかが見えてきます。明日はこの知識を活かして、Goらしいコードを書く方法を学びましょう!