Variadic Functions

A variadic function accepts any number of arguments for its last parameter — fmt.Println, append and max all work this way. This lesson shows how to write one, how to pass an existing slice with ..., and the "functional options" pattern that uses variadic parameters to give functions optional settings.

Declaring a variadic parameter

Put ... before the type of the last parameter. Inside the function, that parameter is a slice:

GO
package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

func main() {
	fmt.Println(sum())
	fmt.Println(sum(5))
	fmt.Println(sum(1, 2, 3, 4))
}
Output
0
5
10

Calling with no arguments is allowed; nums is then a nil slice, and ranging over it simply does nothing.

Passing a slice with ...

If you already have a slice, add ... after it to pass its elements as the variadic arguments:

GO
package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

func main() {
	scores := []int{10, 20, 30}
	fmt.Println(sum(scores...))

	more := append([]int{1, 2}, scores...) // the same syntax with append
	fmt.Println(more)
}
Output
60
[1 2 10 20 30]

You can pass either individual values or one slice with ..., but not a mix: sum(1, scores...) doesn't compile. When you pass a slice this way, the function receives that same slice, not a copy, so changes it makes to the elements are visible to the caller.

Regular parameters first

Only the last parameter can be variadic. Fixed parameters come before it:

GO
package main

import (
	"fmt"
	"strings"
)

func joinWith(sep string, parts ...string) string {
	return strings.Join(parts, sep)
}

func logf(level string, format string, args ...any) {
	fmt.Printf("["+level+"] "+format+"\n", args...)
}

func main() {
	fmt.Println(joinWith(" / ", "home", "docs", "go"))
	logf("INFO", "%d users online, %s mode", 42, "safe")
}
Output
home / docs / go
[INFO] 42 users online, safe mode

...any accepts arguments of any type. That's how fmt.Printf is declared, and passing args... along forwards them unchanged — a common pattern for wrapper functions.

Functional options

Go has no default or named arguments. A popular way to give a constructor optional settings is to accept a variadic list of option functions, each of which changes one setting:

GO
package main

import (
	"fmt"
	"time"
)

type Server struct {
	host    string
	port    int
	timeout time.Duration
}

type Option func(*Server)

func WithPort(p int) Option {
	return func(s *Server) { s.port = p }
}

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

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

func main() {
	a := NewServer("example.com")
	b := NewServer("localhost", WithPort(8080), WithTimeout(5*time.Second))
	for _, s := range []*Server{a, b} {
		fmt.Printf("%s:%d (timeout %v)\n", s.host, s.port, s.timeout)
	}
}
Output
example.com:80 (timeout 30s)
localhost:8080 (timeout 5s)

Callers only mention the settings they care about, and new options can be added later without breaking existing calls. This uses pointers (lesson 37) and closures (next lesson); come back to it once you've read those. For simpler cases, passing a config struct is equally idiomatic.

Try it yourself

Write average(nums ...float64) that returns the mean, and an error when called with no numbers. Call it both with separate values and with a slice.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
5 <nil>
20.333333333333332 <nil>
0 average of no numbers
Next: function values and closures.