Back to Blog

The Go func Keyword: Functions, Methods and Closures

How Go's func keyword works: function signatures, multiple return values and named results, variadic functions, value vs pointer receivers, function values, closures and loop variables, functional options, generic functions, main and init.

The Go func Keyword: Functions, Methods and Closures

The func keyword declares every piece of behavior in a Go program. func parse(body []byte) (Request, error) is a function. func (s *Server) Start() error is a method, because it has a receiver before its name. func(w http.ResponseWriter, r *http.Request) { ... } with no name is a function literal, which can capture the variables around it and is how Go writes closures. One keyword covers all three, and the rules for parameters and results are the same in each (Go spec).

TL;DR

  • A function declaration is func name(params) results { body }. Consecutive parameters of the same type share it, as in clamp(n, lo, hi int).
  • Go has no overloading and no default arguments. Two functions with the same name fail with redeclared in this block. Use a different name, an options struct or functional options.
  • Functions can return several values. The (T, error) pair is how Go reports failure, and _ discards a result you don't need.
  • Named results document what a function returns and let a deferred closure annotate err on the way out.
  • ...T makes a function variadic. Inside, the parameter is a []T, and parts... spreads a slice into the call.
  • A method is a function with a receiver. Use a pointer receiver when the method changes the value or the type holds a sync.Mutex.
  • A pointer-receiver method isn't in the value type's method set, so API{} doesn't satisfy http.Handler when only *API has ServeHTTP.
  • Functions are values. You can store them, pass them to slices.SortFunc or http.HandleFunc, and compare them only to nil.
  • A function literal captures the variables themselves, not copies. Since Go 1.22, every loop iteration gets its own loop variable, so goroutines started in a loop see the value they expect.
  • Functional options (NewServer(addr, opts ...Option)) give constructors optional settings. Since Go 1.27, methods can declare their own type parameters, as in func (c *Client) GetJSON[T any](path string) (T, error).

How do you declare a function in Go?

Write func, a name, the parameter list and the result types. A token parser is a typical small function:

example.gogo
package main

import (
	"errors"
	"fmt"
	"strings"
)

func parseBearer(header string) (string, error) {
	token, ok := strings.CutPrefix(header, "Bearer ")
	if !ok || token == "" {
		return "", errors.New("missing bearer token")
	}
	return token, nil
}

func clamp(n, lo, hi int) int {
	return max(lo, min(n, hi))
}

func main() {
	token, err := parseBearer("Bearer eyJhbGciOi")
	fmt.Println(token, err)

	_, err = parseBearer("Basic dXNlcjpwYXNz")
	fmt.Println(err)

	fmt.Println(clamp(250, 1, 100))
}
example.texttext
eyJhbGciOi <nil>
missing bearer token
100

Types come after names, in parameters and results alike. When several parameters in a row share a type, you write it once, so clamp(n, lo, hi int) takes three ints. A single unnamed result needs no parentheses. Two or more results do.

Function names follow the same visibility rule as every other Go identifier. ParseBearer would be exported and callable from other packages, while parseBearer stays inside its own. The Go package keyword covers exported names in more detail.

Does Go have function overloading or default arguments?

No to both. A package can only have one function with a given name, whatever its parameters look like:

example.gogo
func dial(addr string) error { return nil }

func dial(addr string, timeout time.Duration) error { return nil }
example.texttext
./main.go:7:6: dial redeclared in this block
	./main.go:5:6: other declaration of dial

Default values fail at the parser, before type checking even starts:

example.texttext
./main.go:5:46: syntax error: unexpected = in parameter list; possibly missing comma or )

The standard library shows what Go does instead. net.Dial and net.DialTimeout are two names for two behaviors. http.Server is a struct whose zero fields mean "use the default", so you set only the fields you care about. For constructors with many optional settings, Go code uses functional options, covered further down. The name at each call site tells you which function runs.

How do multiple return values work in Go?

A function can return any number of values, and the caller receives them with a multi-value assignment. The most common shape is a result plus an error, as in parseBearer above. The caller checks the error before touching the result, and the compiler makes sure a call with two results isn't used where one value is expected.

When you don't need one of the values, assign it to the blank identifier _. You can't leave it out entirely. token := parseBearer(h) fails with assignment mismatch: 1 variable but parseBearer returns 2 values.

When should you use named result parameters?

Named results give each result a name and declare it as a variable that starts at its zero value. A return with no values, called a bare return, then returns whatever those variables hold:

example.gogo
func splitHostPort(addr string) (host, port string) {
	for i := len(addr) - 1; i >= 0; i-- {
		if addr[i] == ':' {
			host, port = addr[:i], addr[i+1:]
			return
		}
	}
	host = addr
	return
}

splitHostPort("api.levelupgo.dev:443") returns api.levelupgo.dev and 443. The names help here because two string results would otherwise say nothing about which is which. The Go Code Review Comments suggest bare returns only in short functions like this one. In a 60-line function a bare return hides what is being returned.

Named results matter most when a deferred function needs to edit the error before the caller sees it:

example.gogo
func loadConfig(path string) (cfg Config, err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("load config %s: %w", path, err)
		}
	}()

	data, err := os.ReadFile(path)
	if err != nil {
		return Config{}, err
	}
	err = json.Unmarshal(data, &cfg)
	return cfg, err
}
example.texttext
load config /etc/levelupgo/config.json: open /etc/levelupgo/config.json: no such file or directory

Every return first assigns its values to cfg and err, and then the deferred function runs. Because err is a named result, the closure can wrap it once for every exit path. Without the name, the deferred function has no way to reach the value being returned.

What are variadic functions in Go?

A final parameter written as ...T accepts zero or more arguments of type T. Inside the function it is a plain []T:

example.gogo
func buildURL(base string, segments ...string) string {
	return strings.TrimSuffix(base, "/") + "/" + strings.Join(segments, "/")
}

func main() {
	fmt.Println(buildURL("https://api.levelupgo.dev/", "v1", "users", "42"))
	fmt.Println(buildURL("https://api.levelupgo.dev"))

	parts := []string{"v1", "orders", "1001"}
	fmt.Println(buildURL("https://api.levelupgo.dev", parts...))
}
example.texttext
https://api.levelupgo.dev/v1/users/42
https://api.levelupgo.dev/
https://api.levelupgo.dev/v1/orders/1001

parts... passes an existing slice as the variadic argument without copying it. You can't mix the two forms, so buildURL(base, "v2", parts...) fails:

example.texttext
./main.go:18:58: too many arguments in call to buildURL
	have (string, string, []string...)
	want (string, ...string)

The standard library is full of them. fmt.Println(a ...any), append(s, elems...), errors.Join(errs ...error) and the key-value pairs in slog.Info(msg, args ...any) all take this form.

How do methods work in Go?

A method is a function with a receiver, written in parentheses between func and the name. Receivers are most often structs, which the Go struct keyword covers from fields to embedding. The receiver can be the type itself or a pointer to it, and that choice decides whether the method works on the caller's value or on a copy. A rate limiter with a value receiver shows the difference:

example.gogo
type RateLimiter struct {
	limit int
	used  int
}

func (r RateLimiter) Allow() bool {
	if r.used >= r.limit {
		return false
	}
	r.used++
	return true
}

func main() {
	limiter := RateLimiter{limit: 2}
	for range 4 {
		fmt.Print(limiter.Allow(), " ")
	}
	fmt.Println()
}
example.texttext
true true true true

The limiter never blocks anything. Each call gets a fresh copy of limiter, increments used on the copy and throws it away. Changing the receiver to func (r *RateLimiter) Allow() bool makes the method update the original, and the output becomes true true false false. You don't have to write (&limiter).Allow(). Go takes the address for you when the variable is addressable.

When should you use a pointer receiver?

Use a pointer receiver when any of these hold:

  • The method changes the receiver, like Allow above.
  • The type contains a sync.Mutex or another value that must not be copied.
  • The struct is large, so copying it on every call costs more than following a pointer.
Value receiver func (r T)Pointer receiver func (r *T)
Changes the caller's valueNo, it works on a copyYes
Safe with a sync.Mutex fieldNo, the lock is copiedYes
Callable on a T variableYesYes, Go takes the address
In the method set of TYesNo
In the method set of *TYesYes

The mutex case is a bug go vet catches:

example.gogo
type SessionCache struct {
	mu       sync.Mutex
	sessions map[string]string
}

func (c SessionCache) Get(token string) (string, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	user, ok := c.sessions[token]
	return user, ok
}
example.texttext
main.go:13:9: Get passes lock by value: app.SessionCache contains sync.Mutex

Every call locks its own copy of the mutex, so two goroutines calling Get never wait for each other and the lock protects nothing. Small immutable types such as time.Time or a UserID are fine with value receivers. The Code Review Comments also ask you not to mix the two on one type. If one method needs a pointer receiver, give them all one (Go wiki).

Why doesn't my type implement an interface?

A common cause is a pointer receiver. The method set of a value type T includes only the methods with a T receiver. The method set of *T includes both kinds. So when ServeHTTP has a pointer receiver, only *API is an http.Handler:

example.gogo
type API struct {
	version string
}

func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, a.version)
}

func main() {
	var h http.Handler = API{version: "v1"}
	_ = h
}
example.texttext
./main.go:17:23: cannot use API{…} (value of struct type API) as http.Handler value in variable declaration: API does not implement http.Handler (method ServeHTTP has pointer receiver)

Writing &API{version: "v1"} fixes it. The rule exists because an interface can hold a copy of a value, and a pointer method called on that copy would change something the caller never sees. The Go var keyword shows the var _ http.Handler = (*T)(nil) pattern that checks this at compile time, and the Go interface keyword covers method sets and implicit satisfaction in depth.

What are method values and method expressions?

srv.health without parentheses is a method value. It is a function with the receiver already bound, so you can hand it straight to anything that expects a plain function:

example.gogo
type Server struct {
	version string
}

func (s *Server) health(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "ok %s\n", s.version)
}

func main() {
	srv := &Server{version: "1.4.2"}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", srv.health)

	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, httptest.NewRequest("GET", "/health", nil))
	fmt.Print(rec.Body.String())

	handle := (*Server).health
	fmt.Printf("%T\n", handle)
}
example.texttext
ok 1.4.2
func(*main.Server, http.ResponseWriter, *http.Request)

Many Go servers register routes this way. Handlers are methods on a struct that holds the database, logger and config, and mux.HandleFunc receives the bound method values. (*Server).health is a method expression. It turns the receiver into an ordinary first parameter, which is useful when a table picks which method to call at runtime.

What are function values and function types?

Functions are values in Go. You can assign them to variables, store them in struct fields and maps, pass them as arguments and return them from other functions. The type of a function value is its signature:

example.gogo
type Order struct {
	ID    int64
	Total int64
}

func main() {
	orders := []Order{{ID: 1, Total: 4999}, {ID: 2, Total: 1250}, {ID: 3, Total: 8900}}

	slices.SortFunc(orders, func(a, b Order) int {
		return cmp.Compare(b.Total, a.Total)
	})
	fmt.Println(orders)

	bigOrder := func(o Order) bool { return o.Total > 5000 }
	fmt.Println(slices.IndexFunc(orders, bigOrder))
	fmt.Printf("%T\n", bigOrder)
}
example.texttext
[{3 8900} {1 4999} {2 1250}]
0
func(main.Order) bool

slices.SortFunc doesn't know how to order your structs, so you pass it a comparison function. The same idea runs through the standard library: http.HandleFunc takes a handler function, strings.FieldsFunc takes a separator test and sync.OnceValue takes the function to run once.

The zero value of a function type is nil, and calling a nil function panics. That is also the only comparison Go allows:

example.texttext
./main.go:9:5: invalid operation: health == ready (func can only be compared to nil)

Since you can't compare two functions, they can't be map keys either. When you want a set of handlers, key the map by name instead.

A signature can also get its own name with type, and a named function type can have methods. That is how http.HandlerFunc turns a plain function into an http.Handler, and the Go type keyword walks through it along with a RetryPolicy example.

How do closures work in Go?

A function literal can use variables from the function that surrounds it. It doesn't copy them. It captures the variables themselves, so changes on either side are visible to both. A retry helper shows this:

example.gogo
var errUnavailable = errors.New("503 service unavailable")

func retry(attempts int, fn func() error) error {
	var err error
	for range attempts {
		if err = fn(); err == nil {
			return nil
		}
	}
	return err
}

func main() {
	calls := 0
	err := retry(3, func() error {
		calls++
		return errUnavailable
	})
	fmt.Println(calls, err)
}
example.texttext
3 503 service unavailable

calls lives in main, but the literal increments it, and main sees 3 afterwards. The captured variable stays alive as long as any closure refers to it, even after the surrounding function returns. The compiler moves such variables to the heap when it needs to.

HTTP middleware is one of the most common closures in Go code. The returned handler keeps logger and next from the call that built it:

example.gogo
func logRequests(logger *slog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		next.ServeHTTP(w, r)
		logger.Info("request", "method", r.Method, "path", r.URL.Path)
	})
}

Wrapping a handler and sending DELETE /sessions/42 through it logs level=INFO msg=request method=DELETE path=/sessions/42. Each call to logRequests produces a separate closure with its own next, so you can wrap every route with a different handler. For the full production stack built from closures like this one, including ordering, panic recovery and tests, see Go middleware best practices.

You'll also see two short forms a lot. defer func() { ... }() runs a closure when the function returns, which is how the loadConfig example annotates its error and how recover is called. go func() { ... }() starts a closure in a new goroutine. The trailing () calls the literal. Leave it out and the compiler complains that the expression in defer or go must be a function call.

Did Go 1.22 fix closures in loops?

Yes. Before Go 1.22, a for loop had one loop variable shared by every iteration, and closures captured that single variable. This health checker started three goroutines that often all printed the last URL:

example.gogo
urls := []string{"/health", "/ready", "/metrics"}

var wg sync.WaitGroup
for _, url := range urls {
	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("checking", url)
	}()
}
wg.Wait()

With go 1.21 in go.mod, every line of three separate runs printed checking /metrics, and go vet warned loop variable url captured by func literal. With go 1.22 or later, each iteration has its own url, and the same code checks all three paths in whatever order the goroutines finish (Go blog). The change is tied to the go line in go.mod, so an old module keeps the old behavior until you bump it. The old url := url copy inside the loop is no longer needed, and the forvar modernizer in go fix removes it. The Go for keyword covers the other loop variable rules, such as why changing the range value does not change the slice.

What are functional options in Go?

Functional options combine variadic parameters and closures to give a constructor optional settings without overloading or default arguments. Each option is a function that changes the value being built:

example.gogo
type Server struct {
	addr         string
	readTimeout  time.Duration
	maxBodyBytes int64
	logger       *slog.Logger
}

type Option func(*Server)

func WithReadTimeout(d time.Duration) Option {
	return func(s *Server) {
		s.readTimeout = d
	}
}

func WithMaxBodyBytes(n int64) Option {
	return func(s *Server) {
		s.maxBodyBytes = n
	}
}

func NewServer(addr string, opts ...Option) *Server {
	s := &Server{
		addr:         addr,
		readTimeout:  5 * time.Second,
		maxBodyBytes: 1 << 20,
		logger:       slog.Default(),
	}
	for _, opt := range opts {
		opt(s)
	}
	return s
}

func main() {
	a := NewServer(":8080")
	b := NewServer(":8080", WithReadTimeout(30*time.Second), WithMaxBodyBytes(10<<20))
	fmt.Println(a.readTimeout, a.maxBodyBytes)
	fmt.Println(b.readTimeout, b.maxBodyBytes)
}
example.texttext
5s 1048576
30s 10485760

NewServer(":8080") gets every default, and callers name only what they change. Adding an option later doesn't break any existing call. gRPC's grpc.NewServer(opts ...ServerOption) and many database clients use this pattern. For a type with two or three settings, a plain config struct is simpler and easier to read, so reach for options when the list of settings keeps growing.

How do generic functions work in Go?

A function can declare type parameters in square brackets between its name and its parameters. Map works for any input and output type:

example.gogo
func Map[T, U any](items []T, fn func(T) U) []U {
	out := make([]U, 0, len(items))
	for _, item := range items {
		out = append(out, fn(item))
	}
	return out
}

emails := Map(users, func(u User) string { return u.Email })

The compiler infers T as User and U as string from the arguments, so the call needs no brackets. The standard library's slices, maps and cmp packages are built from generic functions like this.

Until Go 1.27, methods couldn't declare their own type parameters. They could use the type parameters of their receiver type, and nothing more. Go 1.27 removes that limit, so a client can have a generic method that decodes into whatever type the caller asks for (Go 1.27 release notes):

example.gogo
type Client struct {
	baseURL string
	http    *http.Client
}

func (c *Client) GetJSON[T any](path string) (T, error) {
	var out T
	resp, err := c.http.Get(c.baseURL + path)
	if err != nil {
		return out, err
	}
	defer resp.Body.Close()
	err = json.NewDecoder(resp.Body).Decode(&out)
	return out, err
}

u, err := c.GetJSON[User]("/users/1")

Against a test server returning one user, this prints {1 [email protected]} <nil>. The type argument has to be written out here because nothing in "/users/1" tells the compiler what T is. What's new in Go 1.27 covers generic methods and their limits, including why they can't satisfy interface methods.

What are main and init in Go?

Two function names are special. func main() in package main is where a program starts, and the program exits when it returns. It takes no arguments and returns nothing:

example.texttext
./main.go:9:6: func main must have no arguments and no return values

Command-line arguments come from os.Args or the flag package, and the exit code from os.Exit. The Go package keyword covers package main in depth.

func init() runs automatically after the package's variables are initialized and before main. A package can have several init functions, even in one file, and they run in the order they appear. You can't call one yourself. init() in your own code fails with undefined: init. Most init use is registration, such as a database driver adding itself to database/sql. The Go var keyword explains initialization order and when a package-level var is the clearer choice.

Where LevelUpGo fits

LevelUpGo teaches Go through exercises that run real Go code in the browser. Go Basics introduces functions, multiple return values and errors as values. Go Language Deep Dives goes further into variadic functions, named results, panic and recover. The Training Ground has short standalone exercises on signatures, methods and closures for practicing outside a course. For the other 24 reserved words, see Go keywords: all 25 explained.

FAQ

Is func a keyword in Go?

Yes. func is one of Go's 25 reserved keywords, so you can't use it as a name. It declares functions and methods and starts function literals and function types. Go code that needs a variable holding a function usually calls it fn.

Does Go support function overloading or default parameters?

No to both. Each name can be declared only once per package, so a second func dial(...) fails with dial redeclared in this block, and a default value in a parameter list is a syntax error. Go uses distinct names, such as net.Dial and net.DialTimeout, a config struct whose zero fields mean "use the default", like http.Server, or functional options.

Should I use a value or pointer receiver in Go?

Use a pointer receiver when the method modifies the receiver, when the type contains a sync.Mutex or similar, or when the struct is large. Use a value receiver for small types that behave like values, such as time.Time. Keep all methods on one type consistent. Remember that only *T has the pointer methods in its method set, which matters for interface satisfaction.

What is func() in Go?

func() is the type of a function that takes no arguments and returns nothing. Any signature written without a name is a function type, such as func(context.Context) error. You use these types for callbacks, struct fields and parameters like sync.OnceFunc(f func()). func() { ... } with a body is a function literal of that type.

Can you compare functions in Go?

Only to nil. health == ready fails with func can only be compared to nil, and for the same reason a function type can't be a map key. When you need to look handlers up, store them in a map[string]http.HandlerFunc keyed by name.

Sources

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account