Dit is die derde artikel in die "Clean Code in Go" reeks. Skone Kode: Funksie en Foutbestuur in Go: Van Chaos tot Klariteit [Part 1] Skone kode in beweging (deel 2): strukture, metodes en samestelling oor erfenis Inleiding: Interfaces - Go se geheime wapen Ek het gesien dat teams 20-method-interface skep wat onmoontlik word om te toets, te spot of te handhaaf. Dan wonder hulle hoekom Go klunky voel. "Accept interfaces, return structs" - as jy net een Go-idiom gehoor het, is dit waarskynlik hierdie een. Maar waarom is dit so belangrik? Algemene interface foute wat ek ontmoet het: Interface met 10+ metodes: ~45% van enterprise Go kode Definieer koppelvlakke op die implementasie site: ~70% van pakkette - Terugkeer van koppelvlakke in plaas van beton tipes: ~55% van funksies - Gebruik leë koppelvlak{} oral: ~30% van die API's - nil interface vs nil pointer verwarring: ~25% van subtiele bugs Na 8 jaar van werk met Go en die debugging van ontelbare koppelvlak-verwante kwessies, kan ek sê: die korrekte gebruik van koppelvlakke is die verskil tussen kode wat die taal veg en kode wat soos water vloei. Interface tevredenheid: Duck typing vir volwassenes In Go bevredig 'n tipe 'n koppelvlak outomaties, sonder uitdruklike verklaring: // 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 Kleine koppelvlakke: die krag van eenvoud Die enkele metode reël Kyk na Go se standaardbibliotheek: type Reader interface { Read([]byte) (int, error) } type Writer interface { Write([]byte) (int, error) } type Closer interface { Close() error } type Stringer interface { String() string } Een metode - een koppelvlak. hoekom? // 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 segregasie beginsel in aksie // 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! Aanvaar interfaces, terugkeer strukture Hoekom dit saak maak // 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 } Praktiese voorbeeld // 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 } Interface samestelling Integrasie van interaksie // 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 assersies en tipe 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) } } Nyl Interfaces: Die Gotchas // 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 } Kyk na die Nil // 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 } Reële voorbeelde van die standaardbibliotheek io.Reader/Writer – Die grondslag van alles // 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!") }) Patterns en anti-patterns Model: Voorwaardelike Interface Implementasie // 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: te algemene koppelvlakke // 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() } Praktiese tips Definieer koppelvlakke op die verbruikerskant, nie implementasie nie Verkies klein interfaces vir groot Gebruik embedding vir interface-komposisie Moenie grensweëls onnodig terugdraai nie Onthou nil interface versus nil pointer Gebruik tipe bewerings versigtig interface{} is 'n laaste hulpmiddel, nie 'n eerste Integrasie checklist - Interface het 1-3 metodes maksimum - Interface gedefinieer naby gebruik Funksie aanvaar koppelvlakke, nie konkrete tipes nie Funksie gee konkrete tipes terug, nie koppelvlakke nie Geen ongebruikte metodes in koppelvlakke - Type assersies hanteer beide gevalle (ok / nie ok) - Interface{} gebruik slegs waar nodig Konklusie Interfaces is die kleef wat Go-programme saam hou. Hulle maak buigsaam, toetsbaar en onderhoubare kode moontlik sonder komplekse erfenishierarchieë. Onthou: in Go is interfaces impliciet, klein en komposerbaar. In die volgende artikel sal ons pakkette en afhanklikhede bespreek: hoe om kode te organiseer sodat die invoergraaf plat is en afhanklikhede eenrigtend is. Hoe klein is te klein? Hoe besluit jy wanneer om 'n koppelvlak te skep teen die gebruik van 'n beton tipe? Deel jou ervaring in die kommentaar!