OOP#
Go has no classes, no inheritance, no constructors. Object
orientation in Go is structs + methods + interfaces +
embedding. Type satisfies interface structurally (no
implements keyword); composition replaces inheritance;
constructors are plain functions, conventionally named
New / NewXxx.
For the underlying function mechanics, see Functions. For struct fields and other composite types, see Types.
Methods#
A method is a function with a receiver. The receiver appears in parentheses before the function name.
type Counter struct{ n int }
func (c Counter) Get() int { return c.n } // value receiver
func (c *Counter) Inc() { c.n++ } // pointer receiver
var c Counter
fmt.Println(c.Get()) // 0
c.Inc()
fmt.Println(c.Get()) // 1
Value vs pointer receiver.
Pointer receiver lets the method mutate the value and is cheaper for large structs.
Value receiver is safe to call on any value; the operator uses it for tiny structs that mirror primitives.
Consistency matters: pick one and uses it for every method on a given type.
Constructors#
Go has no special constructor syntax. The convention is a
package-level function New / NewXxx that returns the
new value (often a pointer).
type Server struct {
addr string
client *http.Client
}
func NewServer(addr string) *Server {
return &Server{
addr: addr,
client: &http.Client{Timeout: 5 * time.Second},
}
}
s := NewServer("0.0.0.0:8080")
For variants and configuration, use functional options.
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.client.Timeout = d }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, client: &http.Client{}}
for _, opt := range opts { opt(s) }
return s
}
s := NewServer("0.0.0.0:8080", WithTimeout(2*time.Second))
Interfaces#
An interface is a set of method signatures. A type satisfies the interface implicitly; the compiler checks structurally at the assignment site.
type Stringer interface {
String() string
}
type User struct{ Name string }
func (u User) String() string { return "user:" + u.Name }
var s Stringer = User{Name: "rk"} // satisfied automatically
fmt.Println(s) // user:rk
The “accept interfaces, return structs” convention. Function parameters typed as the smallest interface the function actually needs; return values typed as the concrete struct.
func summarize(r io.Reader) (int, error) {
data, err := io.ReadAll(r)
if err != nil { return 0, err }
return len(data), nil
}
// r could be a *os.File, a *bytes.Buffer, an http.Response.Body, …
The empty interface any (alias for interface{}) accepts
any value.
Type assertions#
x.(T) extracts the concrete type from an interface value.
Returns (T, ok) in the two-value form.
var x any = "hello"
s := x.(string) // panics if x is not a string
s, ok := x.(string) // ok=true; safe form
switch v := x.(type) {
case string: fmt.Println("string", v)
case int: fmt.Println("int", v)
default: fmt.Println("other")
}
Embedding#
Go’s substitute for inheritance. An embedded field is declared with just a type (no name); the embedding struct promotes the embedded type’s fields and methods.
type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }
type Server struct {
Logger // embedded
addr string
}
s := Server{Logger: Logger{prefix: "[svc]"}, addr: ":8080"}
s.Log("starting") // promoted method; calls Logger.Log via s.Logger
Embedding composes; it does not inherit. The compiler does not look up “is-a” relationships; it copies the method set up from the embedded type to the outer type.
Embedding an interface inside another interface means “include those method signatures”.
type ReadWriter interface {
io.Reader
io.Writer
}
Interface composition#
Composition by embedding is how the stdlib builds its layered
interfaces (io.Reader, io.ReadWriter,
io.ReadWriteCloser).
type Closer interface { Close() error }
type Reader interface { Read(p []byte) (n int, err error) }
type ReadCloser interface {
Reader
Closer
}
Interface satisfaction at compile time#
The operator asserts a type satisfies an interface at compile time with a blank-identifier declaration. Useful when the satisfaction is part of the package contract and would otherwise only surface at distant call sites.
var _ io.Writer = (*Server)(nil) // *Server must satisfy io.Writer
If *Server later loses a method, this fails at compile time.
Generic methods#
Methods cannot have their own type parameters (an open spec issue); the type parameters live on the receiver 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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
}