Structs, methods and interfaces

Struct literals and embedding, pointer versus value receivers, implicit interface satisfaction, io.Reader and io.Writer, and type switches.

Structs and embedding

type Server struct {
    Addr string        // exported: visible outside the package
    Port int
    log  *slog.Logger  // unexported: package-private
}

// embedding promotes the fields and methods of the inner type
type HTTPServer struct {
    Server             // no field name: a promoted embedded field
    TLS    bool
}

func main() {
    s := Server{Addr: "127.0.0.1", Port: 8080}   // named fields: preferred
    positional := Server{"127.0.0.1", 8080, nil} // works, but breaks on every field change

    h := HTTPServer{Server: s, TLS: true}
    fmt.Println(h.Addr, h.Server.Port)   // promoted field reachable directly

    p := &h.Port                          // pointers to fields are fine
    *p = 9090
}
  • Always construct structs with named fields. Positional literals silently bind to declaration order, so adding a field breaks callers in a way the compiler may not catch across packages.
  • Structs are copied on assignment and on every function call, so passing a large struct by value is a real cost; pass a pointer when the struct is big or must be mutated.
  • Embedding is composition, not inheritance. h.Addr is shorthand for h.Server.Addr, and if the outer type declares the same method, the outer one wins.
  • The zero value of a struct should be usable. A var buf bytes.Buffer is ready to write to, and &sync.Mutex{} is unlocked — design for that instead of requiring a constructor.

Methods and receivers

func (s Server) AddrLine() string {   // value receiver: reads, copies
    return fmt.Sprintf("%s:%d", s.Addr, s.Port)
}

func (s *Server) SetPort(p int) {      // pointer receiver: mutates
    s.Port = p
}

func (s *Server) String() string {     // implements fmt.Stringer
    return s.AddrLine()
}

func main() {
    s := Server{Addr: "localhost", Port: 80}
    s.SetPort(8080)          // Go takes the address automatically for an addressable value
    fmt.Println(s)           // String() is called by fmt

    Server{Addr: "x"}.SetPort(1)   // compile error: literal is not addressable
}
ReceiverUse it when
func (t T)The method only reads, and T is small or already a value type such as time.Time
func (t *T)The method mutates, or the struct is large, or another method on the type already uses a pointer
Mixing both on one typeTechnically legal, but confusing: pick pointer receivers for the whole type once any method needs one
func (t T) String() stringImplementing fmt.Stringer; a value receiver makes both T and *T printable

Interfaces and type switches

type Store interface {
    Get(id string) (Item, bool)
    Put(id string, it Item) error
}

// compile-time assertion: if memStore stops satisfying Store, the build fails here
var _ Store = (*memStore)(nil)

// the two most useful interfaces in the standard library
func copyAll(dst io.Writer, src io.Reader) (int64, error) {
    return io.Copy(dst, src)   // works for files, sockets, buffers, HTTP bodies
}

// type switch: behaviour that depends on the concrete type
func describe(v any) string {
    switch x := v.(type) {
    case nil:
        return "nil"
    case string:
        return "string of length " + strconv.Itoa(len(x))
    case error:
        return "error: " + x.Error()
    case fmt.Stringer:
        return "printable: " + x.String()
    default:
        return fmt.Sprintf("%T", x)
    }
}
  • Satisfaction is implicit: a type implements an interface by having the methods, with no implements keyword and no import of the interface package.
  • Interfaces should be declared by the consumer, not the producer. The package that uses a dependency is the one that knows which two methods it really needs.
  • Keep interfaces small. io.Reader has one method and is therefore implementable everywhere, which is why the whole standard library composes around it.
  • A type assertion x.(T) panics when it fails; the comma-ok form v, ok := x.(T) does not. Always use the comma-ok form outside a type switch.
⚠️
Accept interfaces, return concrete types. A function that returns an interface hides the concrete behaviour behind a smaller contract, which makes it harder to call, extend and test. Declare interfaces at the call site and return structs and pointers from constructors.

FAQ

Why can I call a pointer-receiver method on an addressable value?
The compiler rewrites s.SetPort(8080) as (&s).SetPort(8080) when s is addressable. A map element, a function result and a composite literal are not addressable, which is why those calls fail to compile.
When should I use an interface instead of a concrete type?
When two or more real implementations exist, when tests need a fake, or when you are describing a capability the caller depends on. A single implementation plus a single consumer is usually better served by a struct.

Errors, defer and panic Generics and modern Go idioms

Last refreshed 2026-09-18.