Skip to content
JavaGo
Part III · Functions & Errors

Functions

Multiple return values, first-class functions and closures, and what replaces overloading, default arguments and builders.

Section 10 of 31 5 min read

Functions in Go are first-class values: they can be stored in variables, passed as arguments and returned. They can return several values at once, which is how Go reports errors without exceptions.

Multiple return values

Java — one return value, exceptions for failure
public Message getById(long id) throws SQLException {
    return db.query(id); // failure propagates invisibly
}

try {
    Message m = getById(42L);
} catch (SQLException e) {
    // handle
}
Go — results and error returned together
func GetByID(id int64) (Message, error) {
    msg, err := db.Query(id)
    if err != nil {
        return Message{}, fmt.Errorf("get message %d: %w", id, err)
    }
    return msg, nil // nil error means success
}

msg, err := GetByID(42)
if err != nil {
    // handle it, or return it with context
}

Signature features

variadic, grouped and named parameters
// Consecutive parameters of one type can share it
func Clamp(v, lo, hi int) int { ... }

// Variadic: like Java's String... args. nums is a []int
func Sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

Sum(1, 2, 3)
xs := []int{4, 5}
Sum(xs...) // spread an existing slice

// Named results: documentation, and useful with defer
func ParsePair(s string) (key, value string, err error) {
    key, value, found := strings.Cut(s, "=")
    if !found {
        return "", "", fmt.Errorf("parse %q: missing '='", s)
    }
    return key, value, nil
}

Functions as values: how middleware works

Java — functional interface + lambda
@FunctionalInterface
interface Handler {
    void handle(Message msg) throws Exception;
}

static Handler withLogging(Handler next) {
    return msg -> {
        log.info("handling {}", msg.topic());
        next.handle(msg);
    };
}
Go — function types
// A defined function type (not an alias)
type HandlerFunc func(msg Message) error

// Takes a HandlerFunc, returns one wrapping it
func WithLogging(next HandlerFunc) HandlerFunc {
    return func(msg Message) error {
        slog.Info("handling", "topic", msg.Topic)
        err := next(msg)
        if err != nil {
            slog.Error("handler failed", "err", err)
        }
        return err
    }
}

// Compose by wrapping; the outermost runs first
handler := WithLogging(WithRetry(3, process))

Each wrapper returns a new function

Calls go in, errors come back out

Fig. 10.1Middleware is functions wrapping functions. Each wrapper takes a HandlerFunc and returns a new one, so composing them nests them. A call travels inward through every layer, and the result travels back out through the same layers, each of which can act on it: WithRetry can call process again, and WithLogging logs a failure.

Closures

a closure captures variables, not values
func makeCounter() func() int {
    count := 0 // captured by the returned function
    return func() int {
        count++ // shared, mutable: not "effectively final"
        return count
    }
}

next := makeCounter()
fmt.Println(next(), next(), next()) // 1 2 3

No overloading, no default arguments

Go has neither method overloading nor default parameter values, so Java’s telescoping constructors and builders need a different shape. Use distinct names for distinct operations (strconv.Itoa, strconv.FormatInt), a config struct whose zero values are sensible defaults, or, for public constructors with many optional settings, functional options.

Java — builder
Server srv = Server.builder()
    .addr(":8080")
    .timeout(Duration.ofSeconds(5))
    .maxConns(100)
    .build();

// Plus a Builder class with a field,
// a setter and validation per option.
Go — functional options
type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithMaxConns(n int) Option {
    return func(s *Server) { s.maxConns = n }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

srv := NewServer(":8080", WithTimeout(5*time.Second))
↑ ↓ to navigateEnter to openEsc to close