When you ask AI to "make this code flexible" or "add an abstraction layer," it reaches for interfaces. In Go, interfaces work differently than in Java or TypeScript -- types satisfy them implicitly, with no implements keyword. This is elegant but creates a unique class of AI mistakes you need to spot.
How Go interfaces work
An interface is a type that specifies method signatures. No implementation, just the contract.
type Speaker interface {
Speak() string
}Any type that has a Speak() string method satisfies Speaker. That's it. No declaration needed.
type Dog struct{ Name string }
func (d Dog) Speak() string {
return "Woof! My name is " + d.Name
}
type Cat struct{ Name string }
func (c Cat) Speak() string {
return "Meow! Call me " + c.Name
}
func main() {
var s Speaker
s = Dog{Name: "Rex"}
fmt.Println(s.Speak()) // Woof! My name is Rex
s = Cat{Name: "Whiskers"}
fmt.Println(s.Speak()) // Meow! Call me Whiskers
}Go checks at compile time: "Does Dog have all the methods Speaker requires?" Yes? Then Dog is a Speaker.
Why small interfaces matter
The Go standard libraryWhat is standard library?A collection of ready-made tools that come built into a language - no install required. Covers common tasks like reading files or making web requests. is famous for tiny, focused interfaces:
| Interface | Methods | What it enables |
|---|---|---|
io.Reader | Read([]byte) (int, error) | Any data source: files, network, strings, gzip |
io.Writer | Write([]byte) (int, error) | Any data sink: files, network, buffers, HTTP |
error | Error() string | Any error condition with a message |
fmt.Stringer | String() string | Custom string representation for any type |
One method each. This is intentional.
> // AI generates this -- too rigid
> type Database interface {
> Connect() error
> Query(string) ([]Row, error)
> Insert(Row) error
> Update(Row) error
> Delete(int) error
> Close() error
> }
>
> // Idiomatic Go -- small, composable
> type Querier interface {
> Query(string) ([]Row, error)
> }
>
> type Inserter interface {
> Insert(Row) error
> }
>Functions that only read should accept
Querier, not the full Database. This makes testing trivial and keeps coupling low.Evaluating AI interface design
When AI generates interfaces, run through this checklist:
| Check | Good sign | Bad sign |
|---|---|---|
| Number of methods | 1-3 | 5+ (probably too broad) |
| Defined where consumed | Yes, near the function that uses it | No, in a separate "interfaces" package |
| Named after behavior | Reader, Validator, Handler | IUserService, AbstractManager |
| Used by at least 2 types | Yes | No (premature abstraction) |
| Returns concrete types | Functions return structs | Functions return interfaces (usually wrong) |
The last point deserves emphasis: accept interfaces, return structs.
// Good: accepts any io.Reader, returns concrete type
func ReadConfig(r io.Reader) (*Config, error) { ... }
// Bad: returns an interface (hides what you actually get)
func NewReader() io.Reader { ... }func New() SomeInterface, question whether it should return the concrete struct instead.Interface composition
You can embed interfaces within other interfaces to compose behavior:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}This is how io.ReadWriteCloser is defined in the standard libraryWhat is standard library?A collection of ready-made tools that come built into a language - no install required. Covers common tasks like reading files or making web requests.. Any type implementing both Read and Write automatically satisfies ReadWriter.
The nil interface trap
An interface value is actually a pair: (concrete type, concrete value). This creates one of Go's most infamous gotchas:
type Thing struct{}
func (t *Thing) Method() {}
var thing *Thing = nil
var i interface{} = thing
fmt.Println(i == nil) // false! The interface holds (*Thing, nil)The interface knows the type (*Thing) even though the value is nil. So the interface itself isn't nil.
> func GetThing() error {
> var err *MyError = nil
> // ... some logic that might set err ...
> return err // Returns (*MyError, nil) -- NOT a nil error!
> }
>
> if err := GetThing(); err != nil {
> // This executes even when there's no error!
> }
>The fix: return
nil directly, not a typed nil variable:> func GetThing() error {
> // ... some logic ...
> return nil // Returns (nil, nil) -- truly nil interface
> }
>This bug is subtle enough that even experienced developers miss it. When reviewing AI code that returns interfaces, check whether any code path returns a typed nil.
The empty interface and any
The empty interface interface{} (or its alias any since Go 1.18) matches every type:
func AcceptAnything(x any) {
fmt.Printf("Received: %v (type: %T)\n", x, x)
}
AcceptAnything(42) // int
AcceptAnything("hello") // string
AcceptAnything([]int{1,2,3}) // sliceYou'll see any in JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. unmarshaling, generic containers, and reflection code. It's powerful but throws away type safety.
Common interface patterns
The error interface
The built-in error type is an interface. Any type with an Error() string method qualifies:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error in %s: %s", e.Field, e.Message)
}
func ValidateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "cannot be negative"}
}
return nil
}Sorting with sort.Interface
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
people := []Person{{"Alice", 30}, {"Bob", 25}}
sort.Sort(ByAge(people))Quick reference
| Concept | Syntax | Example |
|---|---|---|
| Define interface | type Name interface { methods } | type Speaker interface { Speak() string } |
| Satisfy interface | Just define the methods | func (d Dog) Speak() string { ... } |
| Empty interface | any or interface{} | func Print(v any) |
| Interface composition | Embed interfaces | type ReadWriter interface { Reader; Writer } |
| Type assertion | value := i.(ConcreteType) | d := s.(Dog) |
| Safe type assertion | value, ok := i.(ConcreteType) | d, ok := s.(Dog) |