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 intis clearer thanint, 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.