Interfaces#
Interfaces are Go’s primary abstraction mechanism. Unlike Java or C#, Go interfaces are satisfied implicitly — a type implements an interface simply by having the right methods. No implements keyword. No declaration of intent.
Defining Interfaces#
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
}An interface is a set of method signatures. Any type that has those methods satisfies the interface.
Implicit Satisfaction#
type File struct { /* ... */ }
func (f *File) Read(p []byte) (n int, err error) { /* ... */ }
func (f *File) Write(p []byte) (n int, err error) { /* ... */ }
// File implicitly satisfies Reader, Writer, and ReadWriter
// No "implements" declaration neededThis decouples the interface definition from the implementation. You can define an interface in one package and satisfy it from another without any import dependency.
Compile-Time Check#
Force a compile-time check that a type satisfies an interface:
var _ Reader = (*File)(nil) // compile error if File doesn't implement Reader
var _ io.ReadWriter = (*File)(nil)The Empty Interface#
interface{} // or 'any' (alias since Go 1.18)The empty interface has no methods, so every type satisfies it. Use it when you need to accept any type:
func Print(v any) {
fmt.Println(v)
}
Print(42)
Print("hello")
Print([]int{1, 2, 3})Avoid overusing any. It discards type safety. Prefer specific interfaces or generics.
Type Assertions#
Extract the concrete type from an interface value:
var r Reader = &File{name: "data.txt"}
// Type assertion — panics if wrong
f := r.(*File)
// Safe type assertion — returns ok=false if wrong
f, ok := r.(*File)
if !ok {
fmt.Println("not a *File")
}Type Switch#
func describe(v any) string {
switch t := v.(type) {
case int:
return fmt.Sprintf("integer: %d", t)
case string:
return fmt.Sprintf("string: %q", t)
case bool:
return fmt.Sprintf("boolean: %t", t)
case nil:
return "nil"
default:
return fmt.Sprintf("other: %T", t)
}
}Key Standard Library Interfaces#
error#
type error interface {
Error() string
}The most used interface in Go. Any type with an Error() string method is an error.
fmt.Stringer#
type Stringer interface {
String() string
}Implement this for custom string representation (like toString() in Java):
func (u User) String() string {
return fmt.Sprintf("%s %s <%s>", u.FirstName, u.LastName, u.Email)
}io.Reader and io.Writer#
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}The composition primitives of Go I/O. Files, network connections, HTTP bodies, buffers, compressors, encrypters — all implement these interfaces. This is why you can pipe anything to anything.
// Copy from any Reader to any Writer
io.Copy(os.Stdout, resp.Body)
io.Copy(file, gzipReader)
io.Copy(hashWriter, file)io.Closer#
type Closer interface {
Close() error
}sort.Interface#
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}Implement these three methods to make any collection sortable:
type ByAge []User
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
sort.Sort(ByAge(users))
// Or use sort.Slice (simpler, since Go 1.8):
sort.Slice(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})http.Handler#
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}Any type with a ServeHTTP method can handle HTTP requests. The http.HandlerFunc adapter lets you use plain functions too:
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("ok"))
})Interface Design Principles#
Keep Interfaces Small#
“The bigger the interface, the weaker the abstraction.” — Rob Pike
// GOOD — small, focused
type Reader interface {
Read(p []byte) (n int, err error)
}
// BAD — kitchen sink
type DataStore interface {
Read(id string) ([]byte, error)
Write(id string, data []byte) error
Delete(id string) error
List(prefix string) ([]string, error)
Watch(id string) (<-chan Event, error)
Backup(path string) error
Restore(path string) error
}Define Interfaces Where They Are Used (Consumer Side)#
// In the consumer package, not the provider package
package handler
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
}
type Handler struct {
repo UserRepository // depends on the interface, not a concrete type
}This is the opposite of Java’s approach (interfaces defined by the provider). In Go, the consumer defines what it needs.
Accept Interfaces, Return Structs#
// GOOD — function accepts an interface
func ProcessData(r io.Reader) error { ... }
// GOOD — function returns a concrete type
func NewServer(addr string) *Server { ... }
// AVOID — returning an interface hides the concrete type
func NewServer(addr string) ServerInterface { ... }Interface Values (Internals)#
An interface value is a tuple of (type, value):
var r io.Reader // (nil, nil) — nil interface
r = os.Stdin // (*os.File, pointer to stdin)
r = &bytes.Buffer{} // (*bytes.Buffer, pointer to buffer)
r = strings.NewReader("hello") // (*strings.Reader, pointer to reader)The Nil Interface Trap#
var p *File = nil
var r io.Reader = p
fmt.Println(r == nil) // false!
// r holds (type=*File, value=nil) — the interface is not nil because it has a typeAn interface is nil only when both its type and value are nil. This is a common source of bugs when returning nil pointers through interfaces.
// BUG
func getReader() io.Reader {
var f *File = nil
return f // returns non-nil interface with nil value
}
// FIX
func getReader() io.Reader {
return nil // returns actual nil interface
}Key Takeaways#
- Interfaces are satisfied implicitly. No
implementskeyword — just have the methods. - Keep interfaces small (1-3 methods).
io.Readerhas one method and is the most powerful interface in Go. - Define interfaces where they are consumed, not where they are implemented.
- Accept interfaces, return concrete types.
- The empty interface (
any) accepts everything but discards type safety. Prefer specific interfaces or generics. - An interface wrapping a nil pointer is NOT nil. This is the most common interface-related bug.