Skip to content

Table of Contents

Structs, Interfaces & Generics

Contents

Structs

type Point struct {
    X, Y int
}

p := Point{1, 2}             // positional (fragile)
p = Point{X: 1, Y: 2}        // keyed (preferred)
p = Point{X: 1}              // Y defaults to 0
var z Point                  // {0 0}
pp := &Point{1, 2}           // pointer to struct
pp.X = 9                     // auto-deref; same as (*pp).X

// Anonymous struct (one-off)
cfg := struct {
    Name string
    Port int
}{Name: "web", Port: 80}

// Comparable if all fields are comparable
Point{1,2} == Point{1,2}     // true

// Empty struct: zero bytes, used for sets and signal channels
done := make(chan struct{})
set := map[string]struct{}{}

Struct tags

Backtick string metadata read via reflection (used by encoding/json, etc.).

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email,omitempty"`  // omit if zero value
    Password  string `json:"-"`                 // never marshal
    Internal  string `json:"-,"`                // field literally named "-"
    Age       int    `json:"age,string"`        // encode number as string
    Created   time.Time `json:"created" db:"created_at"` // multiple tags
}

Tag format: key:"value,opt1,opt2" key2:"...". Read manually:

f, _ := reflect.TypeOf(User{}).FieldByName("Email")
f.Tag.Get("json")           // "email,omitempty"

Embedding

Composition by embedding a type without a field name. Promotes the embedded type's fields and methods.

type Animal struct{ Name string }
func (a Animal) Speak() string { return a.Name + " makes a sound" }

type Dog struct {
    Animal                  // embedded (anonymous field)
    Breed string
}

d := Dog{Animal{"Rex"}, "Lab"}
d.Name                      // "Rex"   (promoted field)
d.Speak()                   // promoted method
d.Animal.Name               // explicit access

// Override by redefining the method on the outer type:
func (d Dog) Speak() string { return d.Name + " barks" }

// Interface embedding builds bigger interfaces:
type ReadWriter interface {
    io.Reader
    io.Writer
}

// Embedding an interface in a struct = "satisfy it, panic if unimplemented"
type partialReader struct{ io.Reader } // only need Read; Writer unset

Embedding is NOT inheritance: the outer type doesn't become the inner type, and method dispatch is static (no virtual override through the embedded type).

Methods & receivers

type Counter struct{ n int }

func (c Counter) Value() int { return c.n }   // value receiver: gets a COPY
func (c *Counter) Inc()      { c.n++ }         // pointer receiver: mutates

c := Counter{}
c.Inc()                     // Go auto-takes &c because c is addressable
c.Value()                   // 1

// Methods can be on any named type, not just structs:
type Celsius float64
func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", c) }

Rule of thumb: use a pointer receiver if the method mutates, the struct is large, or for consistency when any method needs a pointer. Don't mix value and pointer receivers on the same type.

Method sets

What's in a type's method set determines which interfaces it satisfies.

Receiver type Method set of T Method set of *T
func (t T) included included
func (t *T) NOT included included
type Stringer interface{ String() string }
func (c *Counter) String() string { return "..." }

var s Stringer
s = &Counter{}              // OK: *Counter has String
s = Counter{}               // COMPILE ERROR: Counter's method set lacks String

Key consequence: a value type only satisfies an interface if all required methods have value receivers. Map elements and other non-addressable values can't auto-take an address, so pointer-receiver methods aren't callable on them.

Interfaces

Implicitly satisfied — no implements keyword. If a type has the methods, it satisfies the interface.

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rect struct{ W, H float64 }
func (r Rect) Area() float64      { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }

var sh Shape = Rect{3, 4}   // Rect satisfies Shape automatically

// The empty interface holds any value (Go 1.18+ alias: `any`)
var x any = 42
x = "now a string"

// Compile-time satisfaction assertion:
var _ Shape = (*Rect)(nil)  // fails to compile if *Rect stops satisfying Shape

An interface value is a (type, value) pair. A nil interface has both nil. Gotcha: an interface holding a nil concrete pointer is NOT == nil:

func get() error {
    var e *MyErr = nil
    return e                // returns a non-nil interface! (type=*MyErr, value=nil)
}
get() != nil                // true — classic bug; return `nil` explicitly

Keep interfaces small; "accept interfaces, return structs."

Type assertions

Extract the concrete type from an interface.

var i any = "hello"

s := i.(string)             // "hello"; PANICS if i isn't a string
s, ok := i.(string)         // comma-ok form: ok=false, s="" instead of panic
n, ok := i.(int)            // ok=false, n=0

// Assert to another interface:
if rc, ok := r.(io.ReadCloser); ok { rc.Close() }

Type switches

func describe(i any) string {
    switch v := i.(type) {
    case nil:
        return "nil"
    case int:
        return fmt.Sprintf("int %d", v)        // v is int
    case string:
        return fmt.Sprintf("string %q", v)     // v is string
    case []byte, []rune:
        return "byte or rune slice"            // v is `any` here (multiple types)
    case fmt.Stringer:
        return v.String()                      // matches any Stringer
    default:
        return fmt.Sprintf("unknown %T", v)
    }
}

Generics

Type parameters (Go 1.18+). Square brackets declare them.

// Generic function
func Map[T, U any](s []T, f func(T) U) []U {
    r := make([]U, len(s))
    for i, v := range s { r[i] = f(v) }
    return r
}
doubled := Map([]int{1,2,3}, func(x int) int { return x*2 })
strs := Map([]int{1,2}, func(x int) string { return strconv.Itoa(x) })

// Type inference usually lets you omit the brackets at call sites.
// Explicit: Map[int, string]([]int{1}, conv)

// Generic type
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 { return zero, false }
    v := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return v, true
}
var st Stack[string]

Constraints

A constraint is an interface used as a type bound. It can list methods, types, or both.

import "golang.org/x/exp/constraints" // or define your own

// Method constraint
type Stringer interface{ String() string }
func Join[T Stringer](xs []T) string { /* ... */ }

// Type set constraint (the | union; ~ allows named types with that underlying)
type Number interface {
    ~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
    var total T
    for _, x := range xs { total += x }
    return total
}
type MyInt int
Sum([]MyInt{1, 2})          // works thanks to ~int

// Built-in constraints (package "cmp" and predeclared):
//   any         — alias for interface{}
//   comparable  — supports == and != (map keys, equality)
func Index[T comparable](s []T, target T) int {
    for i, v := range s { if v == target { return i } }
    return -1
}

// constraints.Ordered — supports < <= >= > (ints, floats, strings)
import "cmp"
func Max2[T cmp.Ordered](a, b T) T { if a > b { return a }; return b } // Go 1.21+

Notes:

  • ~T (approximation) includes any type whose underlying type is T.
  • comparable permits ==; cmp.Ordered permits ordering operators.
  • Type parameters can't be used for method declarations beyond the receiver's own params, and there are no generic methods (only generic functions/types).