Go's single-method interfaces are the norm, not the exception. In Go, a type satisfies an interface automatically, without explicit declaration. The single method rule is the difference between code that fights language and code that flows like water.Go's single-method interfaces are the norm, not the exception. In Go, a type satisfies an interface automatically, without explicit declaration. The single method rule is the difference between code that fights language and code that flows like water.

Clean Code: Interfaces in Go - Why Small Is Beautiful [Part 3]

2025/11/19 23:00
9 min read
For feedback or concerns regarding this content, please contact us at [email protected]

This is the third article in the "Clean Code in Go" series. Previous Parts:

Clean Code: Functions and Error Handling in Go: From Chaos to Clarity [Part 1]

Clean Code in Go (Part 2): Structs, Methods, and Composition Over Inheritance

Introduction: Interfaces — Go's Secret Weapon

I've watched teams create 20-method interfaces that become impossible to test, mock, or maintain. Then they wonder why Go feels clunky. "Accept interfaces, return structs" — if you've heard only one Go idiom, it's probably this one. But why is it so important? And why are single-method interfaces the norm in Go, not the exception?

\ Common interface mistakes I've encountered: \n - Interfaces with 10+ methods: ~45% of enterprise Go code \n - Defining interfaces at the implementation site: ~70% of packages \n - Returning interfaces instead of concrete types: ~55% of functions \n - Using empty interface{} everywhere: ~30% of APIs \n - nil interface vs nil pointer confusion: ~25% of subtle bugs

\ After 8 years working with Go and debugging countless interface-related issues, I can say: proper use of interfaces is the difference between code that fights the language and code that flows like water.

Interface Satisfaction: Duck Typing for Adults

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

Small Interfaces: The Power of Simplicity

The Single Method Rule

Look at Go's standard library:

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. Why?

// 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!

Accept Interfaces, Return Structs

Why This Matters

// 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 }

Practical Example

// 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 Composition

Embedding Interfaces

// 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: The 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 }

Checking for 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 }

Real Examples From Standard Library

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!") })

Patterns and Anti-Patterns

Pattern: 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() }

Practical Tips

  1. Define interfaces on the consumer side, not implementation
  2. Prefer small interfaces to large ones
  3. Use embedding for interface composition
  4. Don't return interfaces without necessity
  5. Remember nil interface vs nil pointer
  6. Use type assertions carefully
  7. interface{} is a last resort, not a first

Interface Checklist

- Interface has 1-3 methods maximum \n - Interface defined near usage \n - Functions accept interfaces, not concrete types \n - Functions return concrete types, not interfaces \n - No unused methods in interfaces \n - Type assertions handle both cases (ok/not ok) \n - interface{} used only where necessary

Conclusion

Interfaces are the glue that holds Go programs together. They enable flexible, testable, and maintainable code without complex inheritance hierarchies. Remember: in Go, interfaces are implicit, small, and composable.

\ In the next article, we'll discuss packages and dependencies: how to organize code so the import graph is flat and dependencies are unidirectional.

\ What's your take on interface design? How small is too small? How do you decide when to create an interface vs using a concrete type? Share your experience in the comments!

Market Opportunity
Particl Logo
Particl Price(PART)
$0.1503
$0.1503$0.1503
-0.33%
USD
Particl (PART) Live Price Chart
Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact [email protected] for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.

You May Also Like

FCA, crackdown on crypto

FCA, crackdown on crypto

The post FCA, crackdown on crypto appeared on BitcoinEthereumNews.com. The regulation of cryptocurrencies in the United Kingdom enters a decisive phase. The Financial Conduct Authority (FCA) has initiated a consultation to set minimum standards on transparency, consumer protection, and digital custody, in order to strengthen market confidence and ensure safer operations for exchanges, wallets, and crypto service providers. The consultation was published on May 2, 2025, and opened a public discussion on operational responsibilities and safeguarding requirements for digital assets (CoinDesk). The goal is to make the rules clearer without hindering the sector’s evolution. According to the data collected by our regulatory monitoring team, in the first weeks following the publication, the feedback received from professionals and operators focused mainly on custody, incident reporting, and insurance requirements. Industry analysts note that many responses require technical clarifications on multi-sig, asset segregation, and recovery protocols, as well as proposals to scale obligations based on the size of the operator. FCA Consultation: What’s on the Table The consultation document clarifies how to apply rules inspired by traditional finance to the crypto perimeter, balancing innovation, market integrity, and user protection. In this context, the goal is to introduce minimum standards for all firms under the supervision of the FCA, an essential step for a more transparent and secure sector, with measurable benefits for users. The proposed pillars Obligations towards consumers: assessment on the extension of the Consumer Duty – a requirement that mandates companies to provide “good outcomes” – to crypto services, with outcomes for users that are traceable and verifiable. Operational resilience: introduction of continuity requirements, incident response plans, and periodic testing to ensure the operational stability of platforms even in adverse scenarios. Financial Crime Prevention: strengthening AML/CFT measures through more stringent transaction monitoring and structured counterpart checks. Custody and safeguarding: definition of operational methods for the segregation of client assets, secure…
Share
BitcoinEthereumNews2025/09/18 05:40
From Under $0.0025 to $0.25 Over the Next 10 Weeks? Little Pepe (LILPEPE) Named Best Crypto to Buy in 2025 Over Ripple (XRP)

From Under $0.0025 to $0.25 Over the Next 10 Weeks? Little Pepe (LILPEPE) Named Best Crypto to Buy in 2025 Over Ripple (XRP)

The post From Under $0.0025 to $0.25 Over the Next 10 Weeks? Little Pepe (LILPEPE) Named Best Crypto to Buy in 2025 Over Ripple (XRP) appeared on BitcoinEthereumNews.com. The cryptocurrency sector is dynamic and vital for major and minor players alike. With every boom, new categories of tokens are introduced that make new market predictions based on new sets of metrics.  Many believe that, apart from having an appreciated use case that makes it easily attain adoption, Ripple (XRP) has already established itself as a vital part of the blockchain system. But as it turns out, a new competitor, Little Pepe (LILPEPE), has generated significant buzz. Little Pepe is projected to appreciate to 100x its current price of 0.0021, reach 0.25 in 2025, and is considered a top pick for 2025. Ripple (XRP): Dependable but Predictable Ripple has dominated cross-border payment technology for many years. Priced at around $2.98, Ripple remains well supported by partnerships with industry leaders and its increasing contribution to payment processing.  Analysts predict XRP to be at the $7 to $10 range by 2026 and the recent favorable legal rulings Ripple has received in the United States has heightened optimism surrounding the token. For conservative investors, XRP represents stability in an otherwise volatile sector. However, its large market capitalization makes 50x or 100x gains virtually impossible within one cycle. Ripple is a strong asset in the utility sense, but lacks the utility that smaller tokens can bring. Little Pepe (LILPEPE): Presale Energy With a Twist Little Pepe is capturing the attention of investors with its outstanding presale performance. Currently, the presale is in Stage 12, and each stage sells out faster and faster. presale is at $0.0021.  Each stage is selling out faster and faster. Analysts speculate the token could rise to $0.25 within 10 weeks after listing. Such a rise would be one of recent memory’s most remarkable early runs. What makes Little Pepe different is its dual identity. On the surface, it…
Share
BitcoinEthereumNews2025/09/18 15:34
South Korea’s Crypto Crackdown: Tax Agency to Secure Seized Digital Assets with Private Custodian

South Korea’s Crypto Crackdown: Tax Agency to Secure Seized Digital Assets with Private Custodian

BitcoinWorld South Korea’s Crypto Crackdown: Tax Agency to Secure Seized Digital Assets with Private Custodian SEOUL, South Korea – The National Tax Service (NTS
Share
bitcoinworld2026/03/20 16:20