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
public Message getById(long id) throws SQLException {
return db.query(id); // failure propagates invisibly
}
try {
Message m = getById(42L);
} catch (SQLException e) {
// handle
}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
// 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
@FunctionalInterface
interface Handler {
void handle(Message msg) throws Exception;
}
static Handler withLogging(Handler next) {
return msg -> {
log.info("handling {}", msg.topic());
next.handle(msg);
};
}// 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
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
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 3No 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.
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.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))