Skip to main content

Functions

The Building Blocks of Go Programs​

Functions are the primary mechanism for structuring code in Go. They are straightforward to declare, yet packed with features that address real-world programming needs: multiple return values, named returns, variadic parameters, and a clean type system that makes function signatures expressive and self-documenting.

Go functions follow a clear declaration syntax:

func name(parameter list) (return list) {
body
}

Both the parameter list and the return list are optional. The return types come after the parameter list β€” the opposite of C, Java, and many other languages. This design keeps the function name prominent and makes parameter types easy to scan.


Basic Function Declaration​

package main

import "fmt"

// greet takes a name string and returns a greeting string.
func greet(name string) string {
return "Hello, " + name + "!"
}

// add takes two ints and returns their sum.
func add(a, b int) int {
return a + b
}

// printDivider takes no parameters and returns nothing.
func printDivider() {
fmt.Println("---")
}

func main() {
fmt.Println(greet("Gopher"))
fmt.Println(add(3, 4))
printDivider()
}

Output:

Hello, Gopher!
7
---

Parameter Type Shorthand​

When consecutive parameters share the same type, you can list them together and write the type only once:

// Explicit (verbose)
func rect(width int, height int) int { return width * height }

// Shorthand β€” width and height both int
func rectShort(width, height int) int { return width * height }

// Mixed: first pair shares int, last is float64
func mixed(a, b int, c float64) float64 {
return float64(a+b) * c
}

This shorthand works for any number of consecutive same-type parameters and keeps signatures compact.


Multiple Return Values​

Multiple return values are one of Go's most distinctive features. Where other languages require out-parameters, wrapper structs, or exceptions, Go simply lets functions return more than one value. The primary use case is returning a result alongside an error.

package main

import (
"errors"
"fmt"
"strconv"
)

// divide returns the quotient and any error.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

// parseAndDouble converts a string to an int, doubles it, and returns both
// the result and a potential parse error.
func parseAndDouble(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parseAndDouble: %w", err)
}
return n * 2, nil
}

func main() {
if result, err := divide(10, 3); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("10 / 3 = %.4f\n", result)
}

if result, err := divide(5, 0); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}

if v, err := parseAndDouble("21"); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Doubled:", v)
}

if _, err := parseAndDouble("abc"); err != nil {
fmt.Println("Error:", err)
}
}

Output:

10 / 3 = 3.3333
Error: division by zero
Doubled: 42
Error: parseAndDouble: strconv.Atoi: parsing "abc": invalid syntax

Use _ to discard a return value you do not need. The convention is always to check error return values β€” the Go toolchain and golangci-lint will warn you if you silently ignore errors.


Named Return Values​

Go allows you to give names to return values. Named returns are declared inside parentheses just like parameters. Inside the function, they behave as pre-declared variables initialized to their zero values. A bare return (naked return) returns the current values of all named return variables.

package main

import "fmt"

// minMax returns the minimum and maximum of a slice.
// Named returns document the intent directly in the signature.
func minMax(nums []int) (min, max int) {
if len(nums) == 0 {
return // returns 0, 0 (zero values)
}
min, max = nums[0], nums[0]
for _, n := range nums[1:] {
if n < min {
min = n
}
if n > max {
max = n
}
}
return // naked return: returns current min and max
}

// stats computes count, sum, and mean with named returns.
func stats(data []float64) (count int, sum, mean float64) {
count = len(data)
if count == 0 {
return
}
for _, v := range data {
sum += v
}
mean = sum / float64(count)
return
}

func main() {
lo, hi := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
fmt.Printf("min=%d max=%d\n", lo, hi)

lo2, hi2 := minMax(nil)
fmt.Printf("empty: min=%d max=%d\n", lo2, hi2)

n, s, m := stats([]float64{1.5, 2.5, 3.0, 4.0})
fmt.Printf("count=%d sum=%.1f mean=%.2f\n", n, s, m)
}

Output:

min=1  max=9
empty: min=0 max=0
count=4 sum=11.0 mean=2.75

Guidelines for named returns:

  • Use them when the names genuinely document what the values represent (e.g., min, max int is clearer than int, int).
  • Avoid naked returns in long functions β€” they hurt readability because a reader cannot tell what is being returned without scrolling back to the signature.
  • Named returns are often the right choice for defer-based cleanup where you need to modify a return value after the body has executed.

Variadic Functions​

A variadic function accepts a variable number of arguments of a specified type. Declare the last parameter with ...T; inside the function it is a []T slice.

package main

import "fmt"

// sum accepts any number of int arguments.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}

// minVal returns the smallest of the provided ints.
// It panics if called with no arguments.
func minVal(first int, rest ...int) int {
m := first
for _, n := range rest {
if n < m {
m = n
}
}
return m
}

// logf prefixes a format string with a level tag.
func logf(level string, format string, args ...any) {
prefix := fmt.Sprintf("[%s] ", level)
fmt.Print(prefix)
fmt.Printf(format+"\n", args...)
}

func main() {
fmt.Println(sum()) // 0 β€” zero args is valid
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100

// Spread a slice with ...
values := []int{5, 3, 8, 1, 9}
fmt.Println(sum(values...)) // 26
fmt.Println(minVal(values[0], values[1:]...)) // 1

logf("INFO", "server started on port %d", 8080)
logf("WARN", "connection pool at %d%%", 90)
logf("ERROR", "timeout after %s", "30s")
}

Output:

0
6
100
26
1
[INFO] server started on port 8080
[WARN] connection pool at 90%
[ERROR] timeout after 30s

The spread operator ... expands a slice into individual arguments when calling a variadic function. You can only spread the final argument, and only when it matches the variadic element type.


Practical Example: Error + Value Return Pattern​

The idiomatic Go pattern for fallible operations is (result, error). Callers handle errors explicitly at every call site rather than relying on exceptions bubbling up the stack.

package main

import (
"errors"
"fmt"
"os"
"strconv"
"strings"
)

// Config holds parsed application configuration.
type Config struct {
Host string
Port int
Debug bool
Workers int
}

var ErrMissingKey = errors.New("missing required key")

// parseConfig reads a simple KEY=VALUE string map into a Config.
func parseConfig(raw map[string]string) (Config, error) {
var cfg Config
var errs []string

host, ok := raw["host"]
if !ok {
errs = append(errs, "host: "+ErrMissingKey.Error())
} else {
cfg.Host = host
}

portStr, ok := raw["port"]
if !ok {
errs = append(errs, "port: "+ErrMissingKey.Error())
} else {
port, err := strconv.Atoi(portStr)
if err != nil {
errs = append(errs, fmt.Sprintf("port: invalid integer %q", portStr))
} else if port < 1 || port > 65535 {
errs = append(errs, fmt.Sprintf("port: %d out of range [1,65535]", port))
} else {
cfg.Port = port
}
}

if debug, ok := raw["debug"]; ok {
cfg.Debug = strings.EqualFold(debug, "true") || debug == "1"
}

if w, ok := raw["workers"]; ok {
n, err := strconv.Atoi(w)
if err != nil || n < 1 {
errs = append(errs, fmt.Sprintf("workers: invalid value %q", w))
} else {
cfg.Workers = n
}
} else {
cfg.Workers = 4 // default
}

if len(errs) > 0 {
return Config{}, fmt.Errorf("config errors:\n %s", strings.Join(errs, "\n "))
}
return cfg, nil
}

func main() {
good := map[string]string{
"host": "localhost",
"port": "8080",
"debug": "true",
"workers": "8",
}

cfg, err := parseConfig(good)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("Config: %+v\n", cfg)

bad := map[string]string{
"port": "99999",
"workers": "zero",
}

_, err = parseConfig(bad)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
}

Output:

Config: {Host:localhost Port:8080 Debug:true Workers:8}
config errors:
port: 99999 out of range [1,65535]
workers: invalid value "zero"

Practical Example: Variadic Min / Max​

package main

import (
"fmt"
"math"
)

// minFloat64 returns the minimum of one or more float64 values.
func minFloat64(first float64, rest ...float64) float64 {
m := first
for _, v := range rest {
if v < m {
m = v
}
}
return m
}

// maxFloat64 returns the maximum of one or more float64 values.
func maxFloat64(first float64, rest ...float64) float64 {
m := first
for _, v := range rest {
if v > m {
m = v
}
}
return m
}

// clamp restricts value v to [lo, hi].
func clamp(v, lo, hi float64) float64 {
return minFloat64(hi, maxFloat64(lo, v))
}

func main() {
readings := []float64{23.5, 18.1, 31.7, 15.0, 28.4, 22.9}

lo := minFloat64(readings[0], readings[1:]...)
hi := maxFloat64(readings[0], readings[1:]...)
fmt.Printf("Temperature range: %.1fΒ°C – %.1fΒ°C\n", lo, hi)

testValues := []float64{-5, 0, 15, 22, 37, 100}
fmt.Println("\nClamped to [10, 30]:")
for _, v := range testValues {
fmt.Printf(" clamp(%.0f) = %.0f\n", v, clamp(v, 10, 30))
}

// Using math package variadic-style via spread
data := []float64{3.14, 2.71, 1.41, 1.73}
fmt.Printf("\nSmallest constant: %.2f\n", minFloat64(data[0], data[1:]...))
_ = math.Pi // ensure math import used
}

Output:

Temperature range: 15.0Β°C – 31.7Β°C

Clamped to [10, 30]:
clamp(-5) = 10
clamp(0) = 10
clamp(15) = 15
clamp(22) = 22
clamp(37) = 30
clamp(100) = 30

Smallest constant: 1.41

Practical Example: HTTP Handler Function Signatures​

In Go's net/http package, handlers are functions (or types implementing http.Handler) with a fixed signature. Understanding function signatures is essential for writing web services.

package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)

// healthHandler is a plain http.HandlerFunc-compatible function.
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

// echoHandler reads the "msg" query parameter and echoes it back.
func echoHandler(w http.ResponseWriter, r *http.Request) {
msg := r.URL.Query().Get("msg")
if msg == "" {
http.Error(w, "missing 'msg' query parameter", http.StatusBadRequest)
return
}
fmt.Fprintln(w, msg)
}

// withLogging wraps a handler and logs each request.
// It returns an http.HandlerFunc, demonstrating functions returning functions.
func withLogging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next(w, r)
}
}

// addHandler parses two numbers from the URL path and returns their sum.
// Path format: /add/{a}/{b}
func addHandler(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/add/"), "/")
if len(parts) != 2 {
http.Error(w, "usage: /add/{a}/{b}", http.StatusBadRequest)
return
}
a, errA := strconv.ParseFloat(parts[0], 64)
b, errB := strconv.ParseFloat(parts[1], 64)
if errA != nil || errB != nil {
http.Error(w, "invalid numbers", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%.4g + %.4g = %.4g\n", a, b, a+b)
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", withLogging(healthHandler))
mux.HandleFunc("/echo", withLogging(echoHandler))
mux.HandleFunc("/add/", withLogging(addHandler))

fmt.Println("Server listening on :8080")
fmt.Println("Try: curl http://localhost:8080/health")
fmt.Println("Try: curl 'http://localhost:8080/echo?msg=Hello'")
fmt.Println("Try: curl http://localhost:8080/add/3.14/2.71")

// Uncomment to actually start the server:
// log.Fatal(http.ListenAndServe(":8080", mux))
_ = mux
}

Output:

Server listening on :8080
Try: curl http://localhost:8080/health
Try: curl 'http://localhost:8080/echo?msg=Hello'
Try: curl http://localhost:8080/add/3.14/2.71

Expert Tips​

Prefer explicit returns in long functions. Named returns are a documentation tool, not a shortcut. In a function longer than ten lines, a bare return forces readers to scroll to the signature. Use named returns for documentation; use explicit returns for clarity.

Guard variadic functions against empty calls. If your variadic function has no meaningful zero-argument behaviour, accept the first argument as a required parameter (func fn(first T, rest ...T)) rather than crashing at runtime when the slice is empty.

Wrap errors at function boundaries. Use fmt.Errorf("context: %w", err) when returning errors from helper functions. This preserves the original error for errors.Is/errors.As while adding context about where the failure occurred.

Multiple return values are not tuples. You cannot store multiple return values in a single variable or pass them directly to another multi-argument function (with one exception: passing them straight to a variadic). Assign them to separate variables first.

Function signatures are documentation. A signature like func ReadFile(name string) ([]byte, error) tells you everything: what it needs, what it produces, and that it can fail. Design your own signatures with the same care β€” they are the public contract of your code.