Go interfaces are satisfied implicitly — a type implements an interface by having all the required methods, with no implements declaration needed; this structural typing allows retroactive interface satisfaction and decouples packages. Go has no class hierarchy, instead using structs with methods and struct embedding for composition over inheritance.
Java and C++ make you declare class Dog implements Animal. Go does not. If your type has the right methods, it automatically satisfies any interface that requires those methods — even interfaces written after your type. This sounds like a minor syntax difference, but it fundamentally changes how packages relate to each other.
Structs and methods
Go has no class keyword. You define data in a struct and attach behaviour by writing functions with a receiver. The receiver is the type the function belongs to — analogous to this or self in other languages.
package main
import (
"fmt"
"math"
)
// Struct: named collection of fields
type Circle struct {
Radius float64
}
type Rectangle struct {
Width, Height float64
}
// Method: function with a receiver
// (c Circle) is the receiver — c is like "this" in Java
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Pointer receiver: use *T when the method modifies the struct
// or when the struct is large (avoids copying)
func (c *Circle) Scale(factor float64) {
c.Radius *= factor // modifies the original, not a copy
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func main() {
c := Circle{Radius: 5}
fmt.Printf("Circle area: %.2f\n", c.Area())
c.Scale(2)
fmt.Printf("After scale: radius=%.1f, area=%.2f\n", c.Radius, c.Area())
r := Rectangle{Width: 4, Height: 6}
fmt.Printf("Rectangle area: %.2f\n", r.Area())
}Interfaces — implicit satisfaction
An interface defines a set of method signatures. Any type that has all those methods automatically satisfies the interface — no declaration, no registration. This is Go's structural typing (also called "duck typing" at the type level).
package main
import (
"fmt"
"math"
)
// Interface: defines required methods
// Any type with Area() and Perimeter() methods satisfies Shape
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct { Radius float64 }
type Rectangle struct { Width, Height float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
// Function accepts any Shape — works for Circle, Rectangle, or any future type
func printInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
shapes := []Shape{
Circle{Radius: 3},
Rectangle{Width: 4, Height: 5},
}
for _, s := range shapes {
printInfo(s)
}
// No "implements Shape" anywhere — the compiler checks method sets
}Struct embedding — composition over inheritance
Go has no inheritance. Instead, a struct can embed another struct — its fields and methods are promoted to the outer struct. This is composition, not inheritance: the embedded type's methods are accessible directly, but there is no "is-a" relationship. Embedding an interface in a struct (or interface) is also valid and widely used.
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Breathe() {
fmt.Printf("%s breathes air.\n", a.Name)
}
// Dog embeds Animal — gets its fields and methods promoted
type Dog struct {
Animal // embedded (no field name — just the type)
Breed string
}
func (d Dog) Speak() {
fmt.Printf("%s says: Woof!\n", d.Name) // Name promoted from Animal
}
func main() {
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
d.Breathe() // promoted from Animal — works directly
d.Speak()
fmt.Println(d.Name) // promoted field
// Embedding an interface: useful for mocking and composition
// type ReadWriter interface {
// io.Reader // embeds Reader (requires Read method)
// io.Writer // embeds Writer (requires Write method)
// }
}Type assertions and type switches
When you have a value of an interface type, you can recover the underlying concrete type with a type assertion: v, ok := i.(T). If ok is false, the concrete type is not T. A type switch tests multiple types at once — the idiomatic Go way to branch on dynamic type.
package main
import "fmt"
func describe(i any) { // any = interface{} — accepts any value
switch v := i.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %q (len %d)\n", v, len(v))
case bool:
fmt.Printf("bool: %v\n", v)
case []int:
fmt.Printf("[]int of length %d\n", len(v))
default:
fmt.Printf("unknown type: %T\n", v)
}
}
func main() {
describe(42)
describe("hello")
describe(true)
describe([]int{1, 2, 3})
describe(3.14)
// Single type assertion
var i any = "hello"
s, ok := i.(string)
if ok {
fmt.Println("it's a string:", s)
}
// i.(string) without ok panics if the assertion fails
}Generics (Go 1.18+)
Go added generics in version 1.18 (March 2022). Type parameters use square brackets. Type constraints are interfaces — the built-in any constraint accepts all types; comparable accepts types that support ==. The constraints package (now in golang.org/x/exp) provides Ordered, Integer, and other useful constraints.
package main
import "fmt"
// Map applies f to every element of s and returns the results
// T and U are type parameters — constrained to "any"
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// Filter returns elements of s where f returns true
func Filter[T any](s []T, f func(T) bool) []T {
var result []T
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}
// Number constraint: any type that supports arithmetic
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
// Sum works for any numeric type
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
fmt.Println(doubled) // [2 4 6 8 10]
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
fmt.Println(Sum(nums)) // 15
fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6
}io.Reader and io.Writer work — any type with a Read([]byte) (int, error) method is automatically an io.Reader.*T (pointer receiver), only *T satisfies interfaces requiring that method — not T. If a method is on T (value receiver), both T and *T satisfy the interface. Use pointer receivers when the method must modify the struct or the struct is large.Interface representation: iface and eface
At runtime, a Go interface value is a two-word structure. For a non-empty interface (interface{ Method() }), it is an iface: a pointer to the itab (interface table — containing the type descriptor and a method dispatch table) and a pointer to the concrete value. For the empty interface (any / interface{}), it is an eface: a pointer to the type descriptor and a pointer to the value. Assigning a concrete value to an interface variable may allocate on the heap (if the value is too large to fit in a pointer) — this is a common source of allocation in tight loops. Comparing two interface values compares both the type and the value; comparing an interface holding a nil pointer to nil returns false — the interface is non-nil because it has a type pointer, even if the value pointer is nil.
Generics: reification vs erasure
Go's generics use a form of dictionary passing with GCShape stenciling (the GC Shapes proposal, implemented in Go 1.18). Types with the same underlying GC shape (same pointer layout) share a single monomorphised implementation; a dictionary is passed at runtime to distinguish the actual types. This is a middle ground between Java's type erasure (one implementation, boxing overhead) and C++'s full monomorphisation (one implementation per type instantiation, code size cost). The result: Go generics have low code-size overhead but add a small runtime dispatch cost for generic functions. For performance-critical code, benchmarking is advisable.
The io package: interface-based composition
Go's io package defines a small set of single-method interfaces — io.Reader, io.Writer, io.Closer, io.ReadWriter — that form the backbone of the entire standard library's I/O model. Files, network connections, HTTP request bodies, compression streams, buffers, and cryptographic hashers all satisfy io.Reader and/or io.Writer. This is the payoff of implicit interfaces: code written against io.Reader works with any source — a file, a socket, an in-memory buffer — without knowing which. It is the canonical example of Go's interface design principle: accept interfaces, return structs.
The Go Team. The Go Programming Language Specification. go.dev/ref/spec. Sections: "Interface types", "Method sets", "Type parameters". Donovan, A. A. A. & Kernighan, B. W. (2015). The Go Programming Language. Addison-Wesley. Chapter 7 (Interfaces). Go Blog: go.dev/blog/laws-of-reflection — interface internals. Griesemer, R. et al. (2021). Type Parameters Proposal. go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md.