これは、「Clean Code in Go」シリーズの3番目の記事です。 「Clean Code: Functions and Error Handling in Go: From Chaos to Clarity」 Clean Code in Go (Part 2): Structures, Methods, and Composition over Inheritance インタフェース - Go's Secret Weapon 私はチームがテスト、嘲笑、または維持することが不可能になる20メソッドインターフェイスを作成するのを見てきました。それから彼らは、なぜGoがクンクイに感じるのかと疑問に思います。「インターフェイスを受け入れ、構造を返す」 — あなたがたがたった1つのGoイディオムを聞いた場合、それはおそらくこの一つです。 出会ったインタフェースエラー: Common interface mistakes I have encountered: インタフェース 10+ メソッド: ~45% エンタープライズ Go コード 実装サイトでインターフェイスを定義する:パッケージの70% コンクリートタイプの代わりにインタフェースを返す: ~55%の機能 空のインターフェイスをどこでも使用する: ~30%のAPI nil interface vs nil pointer confusion: 微妙なバグの約25% Go で 8 年間作業し、無数のインターフェイス関連の問題をデバッグした後、私は言えます:インターフェイスの適切な使用は、言語と戦うコードと水のように流れるコードの違いです。 インターフェイス 満足度: 大人のためのDuck Typing In Go, a type satisfies an interface automatically, without explicit declaration: タイプは、明示的な宣言なしに、インタフェースを自動的に満たす。 // Interface defines a contract type Writer interface { Write([]byte) (int, error) } // File satisfies Writer automatically type File struct { path string } func (f *File) Write(data []byte) (int, error) { // implementation return len(data), nil } // Buffer also satisfies Writer type Buffer struct { data []byte } func (b *Buffer) Write(data []byte) (int, error) { b.data = append(b.data, data...) return len(data), nil } // Function accepts interface func SaveLog(w Writer, message string) error { _, err := w.Write([]byte(message)) return err } // Usage - works with any Writer file := &File{path: "/var/log/app.log"} buffer := &Buffer{} SaveLog(file, "Writing to file") // OK SaveLog(buffer, "Writing to buffer") // OK 小さなインターフェイス:シンプルさの力 単一方法のルール Goの標準図書館をご覧ください: type Reader interface { Read([]byte) (int, error) } type Writer interface { Write([]byte) (int, error) } type Closer interface { Close() error } type Stringer interface { String() string } One method — one interface. なぜ? // BAD: large interface type Storage interface { Save(key string, data []byte) error Load(key string) ([]byte, error) Delete(key string) error List(prefix string) ([]string, error) Exists(key string) bool Size(key string) (int64, error) LastModified(key string) (time.Time, error) } // Problem: what if you only need Save/Load? // You'll have to implement ALL methods! // GOOD: small interfaces type Reader interface { Read(key string) ([]byte, error) } type Writer interface { Write(key string, data []byte) error } type Deleter interface { Delete(key string) error } // Interface composition type ReadWriter interface { Reader Writer } type Storage interface { ReadWriter Deleter } // Now functions can require only what they need func BackupData(r Reader, keys []string) error { for _, key := range keys { data, err := r.Read(key) if err != nil { return fmt.Errorf("read %s: %w", key, err) } // backup process } return nil } // Function requires minimum - only Reader, not entire Storage Interface Segregation Principle in Action(インターフェース分離の原則) // Instead of one monstrous interface type HTTPClient interface { Get(url string) (*Response, error) Post(url string, body []byte) (*Response, error) Put(url string, body []byte) (*Response, error) Delete(url string) (*Response, error) Head(url string) (*Response, error) Options(url string) (*Response, error) Patch(url string, body []byte) (*Response, error) } // Create focused interfaces type Getter interface { Get(url string) (*Response, error) } type Poster interface { Post(url string, body []byte) (*Response, error) } // Function requires only what it uses func FetchUser(g Getter, userID string) (*User, error) { resp, err := g.Get("/users/" + userID) if err != nil { return nil, fmt.Errorf("fetch user %s: %w", userID, err) } // parse response return parseUser(resp) } // Testing becomes easier type mockGetter struct { response *Response err error } func (m mockGetter) Get(url string) (*Response, error) { return m.response, m.err } // Only need to mock one method, not entire HTTPClient! インタフェース、Return Structures なぜこれが重要なのか // BAD: returning interface func NewLogger() Logger { // Logger is interface return &FileLogger{ file: os.Stdout, } } // Problems: // 1. Hides actual type // 2. Loses access to type-specific methods // 3. Complicates debugging // GOOD: return concrete type func NewLogger() *FileLogger { // concrete type return &FileLogger{ file: os.Stdout, } } // But ACCEPT interface func ProcessData(logger Logger, data []byte) error { logger.Log("Processing started") // processing logger.Log("Processing completed") return nil } 実践例 // Repository returns concrete types type UserRepository struct { db *sql.DB } func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{db: db} } func (r *UserRepository) FindByID(id string) (*User, error) { // SQL query return &User{}, nil } func (r *UserRepository) Save(user *User) error { // SQL query return nil } // Service accepts interfaces type UserFinder interface { FindByID(id string) (*User, error) } type UserSaver interface { Save(user *User) error } type UserService struct { finder UserFinder saver UserSaver } func NewUserService(finder UserFinder, saver UserSaver) *UserService { return &UserService{ finder: finder, saver: saver, } } // Easy to test - can substitute mocks type mockFinder struct { user *User err error } func (m mockFinder) FindByID(id string) (*User, error) { return m.user, m.err } func TestUserService(t *testing.T) { mock := mockFinder{ user: &User{Name: "Test"}, } service := NewUserService(mock, nil) // test with mock } インターフェイス構成 インタフェース インタフェース // Base interfaces type Reader interface { Read([]byte) (int, error) } type Writer interface { Write([]byte) (int, error) } type Closer interface { Close() error } // Composition through embedding type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer } // Or more explicitly type ReadWriteCloser interface { Read([]byte) (int, error) Write([]byte) (int, error) Close() error } Type Assertions and Type Switches について // Type assertion - check concrete type func ProcessWriter(w io.Writer) { // Check if Writer also supports Closer if closer, ok := w.(io.Closer); ok { defer closer.Close() } // Check for buffering if buffered, ok := w.(*bufio.Writer); ok { defer buffered.Flush() } w.Write([]byte("data")) } // Type switch - handle different types func Describe(i interface{}) string { switch v := i.(type) { case string: return fmt.Sprintf("String of length %d", len(v)) case int: return fmt.Sprintf("Integer: %d", v) case fmt.Stringer: return fmt.Sprintf("Stringer: %s", v.String()) case error: return fmt.Sprintf("Error: %v", v) default: return fmt.Sprintf("Unknown type: %T", v) } } nil Interfaces: ザ・ゴッカス // WARNING: classic mistake type MyError struct { msg string } func (e *MyError) Error() string { return e.msg } func doSomething() error { var err *MyError = nil // some logic return err // RETURNING nil pointer } func main() { err := doSomething() if err != nil { // TRUE! nil pointer != nil interface fmt.Println("Got error:", err) } } // CORRECT: explicitly return nil func doSomething() error { var err *MyError = nil // some logic if err == nil { return nil // return nil interface } return err } ニールのチェック // Safe nil check for interface func IsNil(i interface{}) bool { if i == nil { return true } // Check if value inside interface is nil value := reflect.ValueOf(i) switch value.Kind() { case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func: return value.IsNil() } return false } 標準図書館の実例 io.Reader/Writer - Foundation of Everything(すべての基礎) // Copy between any Reader and Writer func Copy(dst io.Writer, src io.Reader) (int64, error) // Works with files file1, _ := os.Open("input.txt") file2, _ := os.Create("output.txt") io.Copy(file2, file1) // Works with network conn, _ := net.Dial("tcp", "example.com:80") io.Copy(conn, strings.NewReader("GET / HTTP/1.0\r\n\r\n")) // Works with buffers var buf bytes.Buffer io.Copy(&buf, file1) http.Handler — Web Server in One Method type Handler interface { ServeHTTP(ResponseWriter, *Request) } // Any type with ServeHTTP method can be a handler type MyAPI struct { db Database } func (api MyAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/users": api.handleUsers(w, r) case "/posts": api.handlePosts(w, r) default: http.NotFound(w, r) } } // HandlerFunc - adapter for regular functions type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) // call the function } // Now regular function can be a handler! http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") }) パターンと反パターン パターン: Conditional Interface Implementation // Optional interfaces for extending functionality type Optimizer interface { Optimize() error } func ProcessData(w io.Writer, data []byte) error { // Basic functionality if _, err := w.Write(data); err != nil { return err } // Optional optimization if optimizer, ok := w.(Optimizer); ok { return optimizer.Optimize() } return nil } オリジナルタイトル: Anti-Pattern: Overly Generic Interfaces // BAD: interface{} everywhere func Process(data interface{}) interface{} { // type assertions everywhere switch v := data.(type) { case string: return len(v) case []byte: return len(v) default: return nil } } // GOOD: specific interfaces type Sized interface { Size() int } func Process(s Sized) int { return s.Size() } 実践Tips 消費者側のインターフェイスを定義し、実装ではなく 小さなインターフェースを大きなインターフェースに好む インタフェース構成のための組み込み 必要なくインタフェースを返すな ナイル インターフェイス vs ナイル ポインタ タイプの主張を慎重に用いる interface{} is a last resort, not a first インターフェイスチェックリスト インターフェイスは1~3メソッド最大 インターフェイス: interface defined near usage Functions accept interfaces, not concrete types. 関数はインターフェイスを承認する。 関数はインタフェースではなく具体型を返します。 インターフェイスに未使用の方法がない タイプ主張は両方のケースに対応します(OK/not OK) インタフェースは必要に応じてのみ使用します。 結論 インターフェイスは、Go プログラムを結びつける粘着剤です。それらは、複雑な相続手順なしに柔軟でテスト可能で維持可能なコードを可能にします。 次の記事では、パッケージと依存性について説明します: 輸入グラフが平らで依存性が単方向であるようにコードを組織する方法。 インタフェースの設計についてのあなたの取り組みは何ですか? どれくらい小さいのが小さすぎますか? インタフェースを作成するときにコンクリートタイプを使用するかをどのように決めますか? コメントであなたの経験を共有してください!