Day 2: イディオマティックGo - 講義
今日の目標
- Goらしいコーディングスタイルを理解する
- 効果的な命名規則を学ぶ
- パッケージ設計の基本を理解する
defer,panic,recoverの適切な使用法を学ぶ- コードレビューで指摘される典型的な問題を回避する
---
イディオマティックGoとは
Idiomatic Go(イディオマティックGo)は、Go言語コミュニティで広く受け入れられている「Goらしい」コードの書き方です。
なぜ重要か:
- コードの一貫性が保たれる
- 他の開発者が読みやすい
- バグが入りにくい
- メンテナンスしやすい
参考資料:
---
命名規則
基本原則
// 良い命名
var userCount int
var maxRetries int
var httpClient *http.Client
// 悪い命名
var user_count int // スネークケースは使わない
var MaxRetries int // パッケージレベルの変数で大文字はNG
var HTTPclient *http.Client // 略語は全部大文字
パッケージ名
// 良いパッケージ名
package http
package json
package strings
// 悪いパッケージ名
package httputils // utilsは避ける
package json_parser // アンダースコア禁止
package myPackage // キャメルケース禁止
ルール:
- 小文字のみ
- 短く、明確
- 複数形は避ける(
stringではなくstringsは例外)
変数名の長さ
// スコープが小さい → 短い名前
for i := 0; i < 10; i++ {
// i, j, k などでOK
}
// スコープが大きい → 説明的な名前
var maxConnectionRetries = 5
var defaultHTTPTimeout = 30 * time.Second
// レシーバー名は1-2文字
func (u *User) GetName() string {
return u.Name
}
func (db *Database) Query() error {
return db.connection.Ping()
}
インターフェース名
// 1メソッド → メソッド名 + er
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// 複数メソッド → 適切な名前
type File interface {
Reader
Writer
Closer
}
ゲッター/セッター
// 悪い例(Java風)
type Person struct {
name string
}
func (p *Person) GetName() string {
return p.name
}
func (p *Person) SetName(name string) {
p.name = name
}
// 良い例(Go風)
type Person struct {
name string
}
// ゲッターにGetは不要
func (p *Person) Name() string {
return p.name
}
// セッターはSetを付ける
func (p *Person) SetName(name string) {
p.name = name
}
---
エラー処理のイディオム
早期リターン
// 悪い例:ネストが深い
func processData(data []byte) error {
if len(data) > 0 {
if valid := validate(data); valid {
if result := process(data); result != nil {
return save(result)
} else {
return errors.New("process failed")
}
} else {
return errors.New("invalid data")
}
} else {
return errors.New("empty data")
}
}
// 良い例:早期リターン
func processData(data []byte) error {
if len(data) == 0 {
return errors.New("empty data")
}
if !validate(data) {
return errors.New("invalid data")
}
result := process(data)
if result == nil {
return errors.New("process failed")
}
return save(result)
}
エラーメッセージ
// 悪い例
return errors.New("Failed to connect to database!") // 大文字、感嘆符
// 良い例
return errors.New("failed to connect to database") // 小文字、簡潔
// さらに良い例:コンテキストを含める
return fmt.Errorf("failed to connect to database %s: %w", dbName, err)
エラーチェックのパターン
// 基本パターン
result, err := doSomething()
if err != nil {
return err
}
// インライン初期化
if err := doSomething(); err != nil {
return err
}
// 複数の戻り値を無視する場合
_, err := doSomething()
if err != nil {
return err
}
// エラーのみの関数
if err := doSomething(); err != nil {
return err
}
---
deferの効果的な使用
基本的な使用法
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 関数終了時に必ず実行される
// ファイル処理
// エラーが起きても file.Close() は実行される
return nil
}
deferのスタック順序
package main
import "fmt"
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("main")
}
// 出力:
// main
// 3
// 2
// 1
deferの落とし穴
package main
import "fmt"
// 問題: deferがループの外で評価される
func badExample() {
for i := 0; i < 5; i++ {
// このdeferは5回スタックされる!
defer fmt.Println(i)
}
}
// 解決策1: 関数で囲む
func goodExample1() {
for i := 0; i < 5; i++ {
func(n int) {
defer fmt.Println(n)
}(i)
}
}
// 解決策2: deferを使わない
func goodExample2() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
戻り値とdefer
package main
import "fmt"
func example() (result int) {
defer func() {
result++ // 戻り値を変更できる
}()
return 42
}
func main() {
fmt.Println(example()) // 43
}
---
panicとrecover
panicの使用場面
package main
import "fmt"
// 回復不可能なエラー
func mustConnect(dsn string) *Connection {
conn, err := connect(dsn)
if err != nil {
panic(fmt.Sprintf("failed to connect: %v", err))
}
return conn
}
// 契約違反(プログラムのバグ)
func divide(a, b int) int {
if b == 0 {
panic("divide by zero")
}
return a / b
}
注意: panicは以下の場合のみ使用してください:
- プログラムが継続できない致命的なエラー
- プログラミングエラー(バグ)の検出
- 初期化フェーズの失敗
通常のエラーにはerrorを返してください。
recoverの使用
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic occurred: %v", r)
}
}()
result = a / b
return result, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Println("結果:", result)
}
}
HTTPサーバーでのrecover
package main
import (
"fmt"
"net/http"
)
func recoverMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("panic: %v\n", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next(w, r)
}
}
func dangerousHandler(w http.ResponseWriter, r *http.Request) {
panic("something went wrong")
}
func main() {
http.HandleFunc("/", recoverMiddleware(dangerousHandler))
http.ListenAndServe(":8080", nil)
}
---
インターフェースのイディオム
小さいインターフェース
// 悪い例:巨大なインターフェース
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
Delete(key string) error
List() ([]string, error)
Clear() error
Count() (int, error)
}
// 良い例:小さく分割
type Reader interface {
Get(key string) ([]byte, error)
}
type Writer interface {
Set(key string, value []byte) error
}
type Deleter interface {
Delete(key string) error
}
// 必要に応じて組み合わせ
type ReadWriter interface {
Reader
Writer
}
受け入れはインターフェース、返すは具象型
// 良い例
func processData(r io.Reader) (*Result, error) {
// インターフェースを受け取る
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
// 具象型を返す
return &Result{Data: data}, nil
}
// 悪い例
func processData(r *bytes.Reader) (io.Reader, error) {
// 具象型を受け取り、インターフェースを返す
// これは柔軟性を失う
}
---
構造体の初期化パターン
コンストラクタ関数
package main
type Config struct {
Host string
Port int
Timeout int
}
// NewXxx パターン
func NewConfig(host string, port int) *Config {
return &Config{
Host: host,
Port: port,
Timeout: 30, // デフォルト値
}
}
// オプション引数が多い場合
type ConfigOption func(*Config)
func WithTimeout(timeout int) ConfigOption {
return func(c *Config) {
c.Timeout = timeout
}
}
func WithPort(port int) ConfigOption {
return func(c *Config) {
c.Port = port
}
}
func NewConfigWithOptions(host string, opts ...ConfigOption) *Config {
config := &Config{
Host: host,
Port: 8080, // デフォルト
Timeout: 30, // デフォルト
}
for _, opt := range opts {
opt(config)
}
return config
}
func main() {
// 使用例
config := NewConfigWithOptions(
"localhost",
WithPort(9000),
WithTimeout(60),
)
}
ゼロ値を利用した設計
package main
import "sync"
// 良い例:ゼロ値で使用可能
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func main() {
var counter Counter // ゼロ値で使える
counter.Increment()
}
---
スライスとマップのイディオム
スライスのプリアロケーション
// 悪い例
func processItems(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i) // 何度も再割り当て
}
return result
}
// 良い例
func processItems(n int) []int {
result := make([]int, 0, n) // 容量を事前確保
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}
// さらに良い例:長さが確定している場合
func processItems(n int) []int {
result := make([]int, n) // 長さと容量を確保
for i := 0; i < n; i++ {
result[i] = i
}
return result
}
空スライスとnilスライス
package main
import (
"encoding/json"
"fmt"
)
func main() {
var nilSlice []int
emptySlice := []int{}
fmt.Println("nil:", nilSlice == nil) // true
fmt.Println("empty:", emptySlice == nil) // false
// JSONエンコード時の違い
nilJSON, _ := json.Marshal(nilSlice)
emptyJSON, _ := json.Marshal(emptySlice)
fmt.Println(string(nilJSON)) // null
fmt.Println(string(emptyJSON)) // []
// 通常はnilスライスを使う
var items []string // nil
items = append(items, "a")
}
マップの存在確認
// 悪い例
value := m["key"]
if value != 0 {
// キーが存在する?値が0でない?
}
// 良い例
value, ok := m["key"]
if ok {
// キーが存在する
}
// さらに良い例:値だけ必要な場合
if value := m["key"]; value != 0 {
// 値が0でない場合の処理
}
---
並行処理の準備
sync.Mutexの基本
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Count:", counter.Value())
}
---
コードレビューで指摘される問題
1. エクスポートされた識別子にコメントがない
// 悪い例
type User struct {
Name string
Age int
}
// 良い例
// User はシステム内のユーザーを表します。
type User struct {
Name string
Age int
}
// Login はユーザーをログインさせます。
func (u *User) Login() error {
// ...
}
2. エラーを無視している
// 悪い例
doSomething() // エラーを無視
// 良い例
_ = doSomething() // 意図的に無視していることを明示
// さらに良い例
if err := doSomething(); err != nil {
return err
}
3. 変数のシャドーイング
// 悪い例
func process() error {
user, err := getUser()
if err != nil {
return err
}
if condition {
user, err := getAnotherUser() // userをシャドーイング
if err != nil {
return err
}
// この user は外側の user とは別の変数
}
// ここの user は最初の user
}
// 良い例
func process() error {
user, err := getUser()
if err != nil {
return err
}
if condition {
anotherUser, err := getAnotherUser()
if err != nil {
return err
}
// 変数名を変えて明確にする
}
}
---
プロダクションレベルのイディオム
パッケージ設計の原則
単一責任の原則(Single Responsibility Principle)
// 悪い例:1つのパッケージに複数の責任
package app
type UserService struct{}
type ProductService struct{}
type OrderService struct{}
type PaymentService struct{}
type EmailService struct{}
// 良い例:責任ごとにパッケージを分割
package user
type Service struct{}
package product
type Service struct{}
package order
type Service struct{}
package payment
type Service struct{}
package notification
type EmailService struct{}
循環依存の回避
// 悪い例:循環依存
// package user
type User struct {
Orders []order.Order // userがorderに依存
}
// package order
type Order struct {
User user.User // orderがuserに依存
}
// 良い例:インターフェースで依存を逆転
// package user
type User struct {
ID string
Name string
}
// package order
type Order struct {
UserID string // IDのみ保持
}
// または共通パッケージを作成
// package domain
type User struct { ID string }
type Order struct { UserID string }
コンテキストの活用
コンテキストの適切な使用
package main
import (
"context"
"fmt"
"time"
)
// データベース操作の例
func fetchUser(ctx context.Context, id string) (*User, error) {
// タイムアウト確認
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// 実際のDB操作
// ...
return &User{ID: id}, nil
}
// HTTP APIの例
func handleRequest(ctx context.Context) error {
// コンテキストに値を設定
ctx = context.WithValue(ctx, "requestID", "req-12345")
// タイムアウト付きコンテキスト
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
return processWithContext(ctx)
}
// ベストプラクティス:コンテキストは最初の引数
func processWithContext(ctx context.Context) error {
// コンテキストから値を取得
if reqID := ctx.Value("requestID"); reqID != nil {
fmt.Println("Request ID:", reqID)
}
return nil
}
コンテキストのアンチパターン
// アンチパターン1:構造体にコンテキストを保存
type Service struct {
ctx context.Context // NG: コンテキストは保存しない
}
// 正しい方法:引数として渡す
type Service struct {
db *Database
}
func (s *Service) FetchUser(ctx context.Context, id string) (*User, error) {
return s.db.QueryUser(ctx, id)
}
// アンチパターン2:コンテキストをnilで渡す
func bad() {
doSomething(nil) // NG
}
// 正しい方法
func good() {
doSomething(context.Background()) // または context.TODO()
}
エラーハンドリングの高度なパターン
カスタムエラー型
package main
import (
"errors"
"fmt"
)
// エラー型の定義
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// センチネルエラー(定数エラー)
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrInvalidInput = errors.New("invalid input")
)
// エラーのラップ
func processUser(id string) error {
user, err := fetchUser(id)
if err != nil {
return fmt.Errorf("failed to process user %s: %w", id, err)
}
if err := validateUser(user); err != nil {
return fmt.Errorf("user validation failed: %w", err)
}
return nil
}
// エラーの判定
func handleError(err error) {
// センチネルエラーの確認
if errors.Is(err, ErrNotFound) {
fmt.Println("リソースが見つかりません")
return
}
// 型アサーションによる詳細取得
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("バリデーションエラー: %s\n", validationErr.Field)
return
}
// その他のエラー
fmt.Printf("予期しないエラー: %v\n", err)
}
エラーグループパターン
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
// 複数の並行処理でエラーを管理
func processMultiple(ctx context.Context, urls []string) error {
g, ctx := errgroup.WithContext(ctx)
// 並行数の制限
g.SetLimit(10)
for _, url := range urls {
url := url // ループ変数のキャプチャ
g.Go(func() error {
return processURL(ctx, url)
})
}
// 全てのゴルーチンの完了を待つ
// いずれかがエラーを返したら、残りはキャンセルされる
if err := g.Wait(); err != nil {
return fmt.Errorf("processing failed: %w", err)
}
return nil
}
func processURL(ctx context.Context, url string) error {
// コンテキストのキャンセル確認
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// 実際の処理
return nil
}
テスタブルなコード設計
依存性注入(Dependency Injection)
package main
import (
"context"
"database/sql"
"fmt"
)
// インターフェースを定義
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
// 実装1: データベース
type DBUserRepository struct {
db *sql.DB
}
func (r *DBUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
// 実際のDB操作
return &User{ID: id}, nil
}
func (r *DBUserRepository) Save(ctx context.Context, user *User) error {
// 実際のDB操作
return nil
}
// 実装2: インメモリ(テスト用)
type InMemoryUserRepository struct {
users map[string]*User
}
func (r *InMemoryUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
if user, ok := r.users[id]; ok {
return user, nil
}
return nil, ErrNotFound
}
func (r *InMemoryUserRepository) Save(ctx context.Context, user *User) error {
r.users[user.ID] = user
return nil
}
// サービス層:リポジトリに依存
type UserService struct {
repo UserRepository // インターフェース型
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.FindByID(ctx, id)
}
// テストコード
func TestUserService(t *testing.T) {
// モックリポジトリを注入
repo := &InMemoryUserRepository{
users: make(map[string]*User),
}
service := NewUserService(repo)
// テスト実行
user, err := service.GetUser(context.Background(), "1")
// ...
}
ログとオブザーバビリティ
構造化ロギング
package main
import (
"context"
"log/slog"
"os"
)
// JSONハンドラーで構造化ログ
func setupLogger() *slog.Logger {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
return slog.New(handler)
}
func processRequest(ctx context.Context, logger *slog.Logger, userID string) {
// コンテキスト情報を含めたログ
logger.InfoContext(ctx, "processing request",
slog.String("userID", userID),
slog.String("operation", "fetch_profile"),
)
// エラーログ
if err := fetchProfile(userID); err != nil {
logger.ErrorContext(ctx, "failed to fetch profile",
slog.String("userID", userID),
slog.Any("error", err),
)
return
}
// 構造化されたログ
logger.InfoContext(ctx, "request completed",
slog.String("userID", userID),
slog.Duration("duration", time.Since(start)),
)
}
// ログレベル付きロガー
type LeveledLogger struct {
logger *slog.Logger
}
func (l *LeveledLogger) Debug(msg string, args ...any) {
l.logger.Debug(msg, args...)
}
func (l *LeveledLogger) Info(msg string, args ...any) {
l.logger.Info(msg, args...)
}
func (l *LeveledLogger) Warn(msg string, args ...any) {
l.logger.Warn(msg, args...)
}
func (l *LeveledLogger) Error(msg string, args ...any) {
l.logger.Error(msg, args...)
}
パフォーマンス最適化のイディオム
文字列連結の最適化
package main
import (
"bytes"
"strings"
)
// 悪い例:+演算子での連結(遅い)
func badConcat(strs []string) string {
result := ""
for _, s := range strs {
result += s // 毎回新しい文字列を作成
}
return result
}
// 良い例1:strings.Builder(推奨)
func goodConcat1(strs []string) string {
var sb strings.Builder
sb.Grow(len(strs) * 10) // 容量の事前確保
for _, s := range strs {
sb.WriteString(s)
}
return sb.String()
}
// 良い例2:strings.Join(シンプルな場合)
func goodConcat2(strs []string) string {
return strings.Join(strs, "")
}
// 良い例3:bytes.Buffer(バイト操作が必要な場合)
func goodConcat3(strs []string) string {
var buf bytes.Buffer
for _, s := range strs {
buf.WriteString(s)
}
return buf.String()
}
メモリアロケーションの削減
package main
// 悪い例:スライスの頻繁な再割り当て
func badAllocation(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i) // 容量不足時に再割り当て
}
return result
}
// 良い例:事前にメモリを確保
func goodAllocation(n int) []int {
result := make([]int, 0, n) // 容量を事前確保
for i := 0; i < n; i++ {
result = append(result, i) // 再割り当てなし
}
return result
}
// さらに良い例:インデックスアクセス
func bestAllocation(n int) []int {
result := make([]int, n) // 長さと容量を確保
for i := 0; i < n; i++ {
result[i] = i // appendより高速
}
return result
}
// ポインタの適切な使用
type LargeStruct struct {
Data [1024]byte
}
// 悪い例:大きな構造体を値渡し
func badPass(s LargeStruct) {
// 構造体全体がコピーされる
}
// 良い例:ポインタ渡し
func goodPass(s *LargeStruct) {
// ポインタのみコピー(8バイト)
}
実践的なデザインパターン
オプションパターン(Functional Options Pattern)
package main
import "time"
type Server struct {
host string
port int
timeout time.Duration
maxConns int
enableMetrics bool
}
// オプション関数の型
type ServerOption func(*Server)
// オプション関数の定義
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) ServerOption {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConns(maxConns int) ServerOption {
return func(s *Server) {
s.maxConns = maxConns
}
}
func WithMetrics(enable bool) ServerOption {
return func(s *Server) {
s.enableMetrics = enable
}
}
// コンストラクタ
func NewServer(host string, opts ...ServerOption) *Server {
// デフォルト値
server := &Server{
host: host,
port: 8080,
timeout: 30 * time.Second,
maxConns: 100,
enableMetrics: false,
}
// オプションを適用
for _, opt := range opts {
opt(server)
}
return server
}
// 使用例
func main() {
// デフォルト値で作成
server1 := NewServer("localhost")
// カスタム設定で作成
server2 := NewServer("localhost",
WithPort(9000),
WithTimeout(60*time.Second),
WithMaxConns(200),
WithMetrics(true),
)
}
シングルトンパターン(sync.Onceを使用)
package main
import (
"sync"
)
type Config struct {
DatabaseURL string
APIKey string
}
var (
instance *Config
once sync.Once
)
// GetConfig はシングルトンインスタンスを返します
func GetConfig() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: loadDatabaseURL(),
APIKey: loadAPIKey(),
}
})
return instance
}
// または、init関数を使用
var globalConfig *Config
func init() {
globalConfig = &Config{
DatabaseURL: loadDatabaseURL(),
APIKey: loadAPIKey(),
}
}
func GetGlobalConfig() *Config {
return globalConfig
}
ビルダーパターン
package main
import "time"
type Query struct {
table string
columns []string
where map[string]interface{}
orderBy string
limit int
offset int
timeout time.Duration
}
type QueryBuilder struct {
query *Query
}
func NewQueryBuilder(table string) *QueryBuilder {
return &QueryBuilder{
query: &Query{
table: table,
columns: []string{"*"},
where: make(map[string]interface{}),
},
}
}
func (b *QueryBuilder) Select(columns ...string) *QueryBuilder {
b.query.columns = columns
return b
}
func (b *QueryBuilder) Where(field string, value interface{}) *QueryBuilder {
b.query.where[field] = value
return b
}
func (b *QueryBuilder) OrderBy(field string) *QueryBuilder {
b.query.orderBy = field
return b
}
func (b *QueryBuilder) Limit(limit int) *QueryBuilder {
b.query.limit = limit
return b
}
func (b *QueryBuilder) Offset(offset int) *QueryBuilder {
b.query.offset = offset
return b
}
func (b *QueryBuilder) Timeout(timeout time.Duration) *QueryBuilder {
b.query.timeout = timeout
return b
}
func (b *QueryBuilder) Build() *Query {
return b.query
}
// 使用例
func main() {
query := NewQueryBuilder("users").
Select("id", "name", "email").
Where("age", 25).
Where("active", true).
OrderBy("created_at").
Limit(10).
Offset(0).
Timeout(5 * time.Second).
Build()
}
OSSプロジェクトから学ぶイディオム
Kubernetesスタイルのクライアントインターフェース
package main
import "context"
// リソース操作のインターフェース
type Interface interface {
Users() UserInterface
Products() ProductInterface
}
type UserInterface interface {
Get(ctx context.Context, name string) (*User, error)
List(ctx context.Context) (*UserList, error)
Create(ctx context.Context, user *User) (*User, error)
Update(ctx context.Context, user *User) (*User, error)
Delete(ctx context.Context, name string) error
}
type ProductInterface interface {
Get(ctx context.Context, name string) (*Product, error)
List(ctx context.Context) (*ProductList, error)
Create(ctx context.Context, product *Product) (*Product, error)
Update(ctx context.Context, product *Product) (*Product, error)
Delete(ctx context.Context, name string) error
}
// 実装
type Client struct {
baseURL string
}
func (c *Client) Users() UserInterface {
return &userClient{client: c}
}
func (c *Client) Products() ProductInterface {
return &productClient{client: c}
}
type userClient struct {
client *Client
}
func (u *userClient) Get(ctx context.Context, name string) (*User, error) {
// 実装
return nil, nil
}
// 使用例
func main() {
client := &Client{baseURL: "https://api.example.com"}
// Kubernetesライクな使用方法
user, err := client.Users().Get(context.Background(), "user-1")
if err != nil {
// エラーハンドリング
}
}
Prometheusスタイルのメトリクス
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// カウンター:増加のみ
requestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "endpoint", "status"},
)
// ゲージ:増減可能
activeConnections = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "active_connections",
Help: "Number of active connections",
},
)
// ヒストグラム:分布の測定
requestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "endpoint"},
)
)
func handleRequest(method, endpoint string) {
timer := prometheus.NewTimer(requestDuration.WithLabelValues(method, endpoint))
defer timer.ObserveDuration()
activeConnections.Inc()
defer activeConnections.Dec()
// リクエスト処理
status := "200"
requestsTotal.WithLabelValues(method, endpoint, status).Inc()
}
---
まとめと次のステップ
重要なポイント
さらに学ぶべきリソース
次のステップ
Day 3では、これらのイディオムを活かしてインターフェースの設計と実装を学びます。プロダクションレベルのコードを書くための実践的なスキルを身につけましょう。