Go interfaces describe behavior, not data. A struct holds the data, methods attach behavior to it, and an interface lists the methods a type must have. Any type that has those methods satisfies the interface automatically. There is no implements keyword, no class hierarchy and no inheritance.
These few rules replace most of what classes do in Java or C#. This guide covers structs, value and pointer receivers, interfaces, polymorphism, any, type assertions, type switches and the nil-interface trap.
Structs: defining your own data types
A struct groups related fields into a new named type:
type User struct {
ID int64
Name string
Email string
Admin bool
}
func main() {
u := User{ID: 1, Name: "Ana", Email: "[email protected]"}
fmt.Println(u.Name) // Ana
var empty User // zero value: 0, "", "", false
fmt.Println(empty.Admin) // false
}A few things worth knowing early:
- Zero values are usable. An uninitialized struct has every field set to its zero value. Good Go types are designed so the zero value is valid, like
sync.Mutexorbytes.Buffer. - Use field names in literals. Positional literals like
User{1, "Ana", "[email protected]", false}break when someone adds a field. - Exported vs unexported.
Nameis visible outside the package,nameis not. - Struct tags attach metadata for packages like
encoding/json:Email string `json:"email"`. - Structs are values. Assigning
b := acopies every field. Two structs are comparable with==if all their fields are comparable.
You can also create new types from existing ones. type Celsius float64 is a distinct type with float64 as its underlying type. You can't mix it with a plain float64 without a conversion, and you can give it its own methods.
Methods and receivers in Go
A method is a function with a receiver, the value it's called on:
type Rect struct {
Width, Height float64
}
// Method: belongs to Rect, called as r.Area()
func (r Rect) Area() float64 {
return r.Width * r.Height
}
// Function: standalone, called as Area(r)
func Area(r Rect) float64 {
return r.Width * r.Height
}Both compute the same thing, but only the method belongs to the type's method set, which is what interfaces check. You can define methods on any named type in the same package (func (c Celsius) String() string), but not on types from other packages such as int or time.Time.
Value receivers vs pointer receivers
type Counter struct {
n int
}
func (c Counter) IncValue() { c.n++ } // works on a copy, change is lost
func (c *Counter) IncPointer() { c.n++ } // works on the original
func main() {
c := Counter{}
c.IncValue()
c.IncPointer() // Go takes &c for you because c is addressable
fmt.Println(c.n) // 1
}A value receiver gets a copy. A pointer receiver gets the address, so changes are visible to the caller.
| Use a pointer receiver when... | A value receiver is fine when... |
|---|---|
| The method modifies the receiver | The type is small and immutable (e.g. time.Time, Point) |
| The struct is large and copying is wasteful | The type is a map, func or channel |
The struct contains a sync.Mutex or similar that must not be copied | You want callers to be sure the method has no side effects |
| Other methods on the type already use pointer receivers |
The last row matters: be consistent. If any method needs a pointer receiver, give all methods on that type pointer receivers. Mixing them makes method sets confusing, as the next section shows.
Go interfaces: defining behavior
An interface is a set of method signatures. Any type that has all of those methods satisfies it:
type Generator interface {
Generate() ([]byte, error)
}
type PDF struct{ Content string }
type CSV struct{ Lines []string }
func (p PDF) Generate() ([]byte, error) {
return []byte("%PDF " + p.Content), nil
}
func (c CSV) Generate() ([]byte, error) {
return []byte(strings.Join(c.Lines, "\n")), nil
}
func Export(g Generator) error {
data, err := g.Generate()
if err != nil {
return fmt.Errorf("generate: %w", err)
}
fmt.Printf("exported %d bytes\n", len(data))
return nil
}PDF and CSV never mention Generator. They satisfy it because they have a Generate() ([]byte, error) method. This is implicit implementation, and it lets you define an interface for types you don't own. If a third-party client has a Send(ctx, msg) error method, declare a one-method interface in your package and swap in a fake for tests.
Method sets and pointer receivers
If Generate had a pointer receiver, only *PDF would satisfy the interface:
func (p *PDF) Generate() ([]byte, error) { /* ... */ }
var g Generator = PDF{} // compile error: method Generate has pointer receiver
var g Generator = &PDF{} // OKThe rule: the method set of T contains only value-receiver methods. The method set of *T contains both. When the compiler says a type "does not implement" an interface, this is usually why.
Checking implementation at compile time
With implicit implementation, a typo in a method name only surfaces where the value is used. A blank assignment makes the check explicit:
var _ Generator = (*PDF)(nil)
var _ Generator = CSV{}It costs nothing at runtime and fails the build if the type drifts.
Keep interfaces small
The most useful interfaces in the standard library have one or two methods: io.Reader, io.Writer, fmt.Stringer, error. Small interfaces are easy to satisfy, fake and compose. Two habits help:
- Define interfaces where they're used. The consumer knows which methods it needs.
- Accept interfaces, return structs. Take the narrowest interface you need, and return concrete types so callers get the full API.
This is the same idea that ports-and-adapters design builds on. See hexagonal architecture with ports and adapters for how it scales to a whole service.
Polymorphism with interfaces
Polymorphism in Go means code that works with any type satisfying an interface:
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }
func (r Rect) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func main() {
shapes := []Shape{Rect{Width: 3, Height: 4}, Circle{R: 1}}
total := 0.0
for _, s := range shapes {
total += s.Area()
}
fmt.Printf("total area: %.2f\n", total)
}The loop doesn't care which concrete type it holds, and adding a Triangle doesn't change it.
Embedding instead of inheritance
Go reuses code through embedding. An embedded struct's fields and methods are promoted to the outer type:
type Timestamps struct {
CreatedAt, UpdatedAt time.Time
}
func (t *Timestamps) Touch() { t.UpdatedAt = time.Now() }
type Order struct {
Timestamps // embedded
ID int64
Total int64
}
func main() {
o := &Order{ID: 7}
o.Touch() // promoted method
fmt.Println(o.UpdatedAt) // promoted field
}This is composition, not inheritance: you can't pass an Order where a Timestamps is expected. Promoted methods do count toward the method set, though, so *Order satisfies any interface *Timestamps satisfies.
Interfaces embed too: io.ReadWriter is declared as io.Reader and io.Writer embedded in one interface.
The empty interface and any
An interface with no methods, interface{}, is satisfied by every type. Since Go 1.18, any is a built-in alias for it, and the two are identical.
func Describe(v any) {
fmt.Printf("%v (%T)\n", v, v)
}
Describe(42) // 42 (int)
Describe("hi") // hi (string)
Describe([]int{1}) // [1] ([]int)any fits where the type truly isn't known until runtime, such as fmt.Println or decoding JSON into map[string]any. The cost is that the compiler can no longer check anything for you. Before reaching for any, ask whether a small interface or a type parameter (func Max[T cmp.Ordered](a, b T) T) would express the intent better.
Type assertions
A type assertion gets the concrete value back out of an interface:
var v any = "hello"
s := v.(string) // OK: s == "hello"
n := v.(int) // panics: interface conversion
n, ok := v.(int) // comma-ok form never panics
if !ok {
fmt.Println("not an int")
}Use the single-value form only when a wrong type is a programming error. Otherwise use comma-ok.
You can also assert to another interface, which checks for an optional capability:
if s, ok := v.(fmt.Stringer); ok {
fmt.Println(s.String())
}io.Copy, for example, checks whether the source implements io.WriterTo to take a faster path.
Type switches
When there are several possible types, a type switch is cleaner than a chain of assertions:
func Format(v any) string {
switch x := v.(type) {
case nil:
return "null"
case string:
return strconv.Quote(x) // x is a string here
case int, int64:
return fmt.Sprint(x) // x is any here: more than one type in the case
case fmt.Stringer:
return x.String()
default:
return fmt.Sprintf("%v", x)
}
}Inside each single-type case, x has that concrete type. In a case with several types, x keeps the interface type. Cases are checked in order, so put specific types before broad interfaces.
For errors, prefer errors.As over a type switch, because it also looks through wrapped errors. Go error handling and project layout covers that in detail.
The nil interface trap
An interface value holds two things: a type and a value. It's nil only when both are nil. This bites people when returning errors:
type NotFound struct{ Key string }
func (e *NotFound) Error() string { return e.Key + " not found" }
func find(key string) error {
var err *NotFound // nil pointer
if key == "missing" {
err = &NotFound{Key: key}
}
return err // returns a non-nil error even when err is a nil pointer
}
func main() {
if err := find("ok"); err != nil {
fmt.Println("unexpected:", err) // this runs
}
}The returned interface has type *NotFound and value nil, so it isn't equal to nil. The fix is to return a literal nil on the success path, and to declare error variables as error rather than a concrete pointer type.
FAQ
Does Go have classes?
No. Structs, methods, interfaces and embedding cover what classes are usually used for, without inheritance.
Should I use a pointer or value receiver?
Use a pointer receiver if the method modifies the receiver, the struct is large, or it contains a mutex. Otherwise a value receiver is fine. Pick one style per type and stick with it.
How do I check that a type implements an interface in Go?
Add var _ MyInterface = (*MyType)(nil) at package level. The build fails if the type is missing a method.
What is the difference between any and interface{}?
None. any is an alias for interface{} introduced in Go 1.18. Use any in new code because it's shorter.
Why is my error not nil when I returned a nil pointer?
Because the interface holds a type (*MyErr) alongside the nil value. Return a literal nil instead of a typed nil pointer.
Key takeaways
- Model data with structs and keep their zero values useful.
- Use pointer receivers when methods mutate state, and be consistent across the type.
- Keep interfaces small and define them where they're consumed.
- Add a compile-time check for types that must satisfy an interface.
- Prefer comma-ok assertions and type switches over panicking assertions, and
errors.Asfor errors. - Never return a typed nil pointer as an
error.
Next, Go slices, maps and pointers explains what gets copied and what gets shared. If your team is designing Go services and wants a second opinion on the structure, Vectorkub builds and reviews backend systems like these.
