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.Addris shorthand forh.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.Bufferis 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
}| Receiver | Use 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 type | Technically legal, but confusing: pick pointer receivers for the whole type once any method needs one |
func (t T) String() string | Implementing 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
implementskeyword 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.Readerhas 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 formv, 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.
Related
Errors, defer and panic Generics and modern Go idioms
Last refreshed 2026-09-18.