Go Specification: Function Declarations¶
Source: https://go.dev/ref/spec#Function_declarations Sections: Function declarations, Function types, Function literals, Calls, Return statements
1. Spec Reference¶
| Field | Value |
|---|---|
| Official Spec | https://go.dev/ref/spec#Function_declarations |
| Function types | https://go.dev/ref/spec#Function_types |
| Function literals | https://go.dev/ref/spec#Function_literals |
| Calls | https://go.dev/ref/spec#Calls |
| Return statements | https://go.dev/ref/spec#Return_statements |
| Go Version | Go 1.0+ (generic functions added in Go 1.18) |
Official definition from the spec:
"A function declaration binds an identifier, the function name, to a function. A function declaration may omit the body. Such a declaration provides the signature for a function implemented outside Go, such as an assembly routine."
"A function type denotes the set of all functions with the same parameter and result types. The value of an uninitialized variable of function type is nil."
2. Formal Grammar (EBNF)¶
From the Go Language Specification:
FunctionDecl = "func" FunctionName [ TypeParameters ] Signature [ FunctionBody ] .
FunctionName = identifier .
FunctionBody = Block .
Signature = Parameters [ Result ] .
Result = Parameters | Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = ParameterDecl { "," ParameterDecl } .
ParameterDecl = [ IdentifierList ] [ "..." ] Type .
FunctionType = "func" Signature .
FunctionLit = "func" Signature FunctionBody .
Where: - FunctionName is the bound identifier visible at package scope. - TypeParameters (Go 1.18+) introduces generic type parameters. - Signature is the parameter list plus optional results — the unnamed function's "type". - FunctionBody is a Block. Omitting the body declares an externally implemented function (assembly stub). - Result may be a single type (no parens needed) or a parenthesized parameter list (for multiple results or named results). - ParameterDecl may bind names to types or list types only (in function-type expressions).
The forms at a glance:
func name() { } // no params, no result
func name(x int) int { } // single param, single result
func name(x, y int) int { } // grouped params (same type)
func name(x int, y string) bool { } // mixed param types
func name() (int, error) { } // multiple results
func name() (n int, err error) { } // named results
func name(args ...int) int { } // variadic (last param)
3. Core Rules & Constraints¶
3.1 The func Keyword Is the Only Way to Declare a Function¶
Go has exactly one keyword for function declaration: func. There is no function, def, fn, or procedure. The same keyword is reused for function literals (anonymous functions) and method declarations.
package main
import "fmt"
func greet(name string) string {
return "Hello, " + name
}
func main() {
fmt.Println(greet("World"))
}
3.2 Functions Are First-Class Values¶
A function in Go is a value of a function type. It can be: - assigned to a variable, - passed as an argument, - returned from another function, - stored in a struct field, slice, or map, - compared to nil (but not to other functions).
package main
import "fmt"
func add(a, b int) int { return a + b }
func main() {
var op func(int, int) int = add
fmt.Println(op(2, 3)) // 5
ops := map[string]func(int, int) int{
"add": add,
}
fmt.Println(ops["add"](10, 20)) // 30
}
3.3 Named Functions Cannot Be Nested¶
Unlike JavaScript or Python, you cannot declare a named function inside another function. Only function literals (anonymous functions) may appear inside a function body.
package main
func outer() {
// func inner() { } // compile error: nested func not allowed
inner := func() {} // OK: anonymous function literal
inner()
}
func main() { outer() }
3.4 Parameter Lists¶
Parameters of the same type may be grouped, with the type appearing only on the last name. Names are required for all parameters in a declaration that has a body; in a function-type expression, names may be omitted entirely.
package main
import "fmt"
// Same type, grouped:
func sum3(a, b, c int) int { return a + b + c }
// Mixed types:
func mixed(name string, age int, active bool) {}
// Function-type expression (names optional):
type BinaryOp func(int, int) int
func main() {
fmt.Println(sum3(1, 2, 3))
mixed("Ada", 30, true)
}
3.5 Result List¶
The result list is optional. If present and there is exactly one unnamed result, parentheses may be omitted. Multiple results, or any named results, require parentheses.
package main
import "fmt"
func noResult() {}
func oneResult() int { return 1 }
func twoResults() (int, int) { return 1, 2 }
func named() (n int, err error) {
n = 42
return // naked return uses named results
}
func main() {
noResult()
fmt.Println(oneResult())
a, b := twoResults()
fmt.Println(a, b)
n, err := named()
fmt.Println(n, err)
}
3.6 The Return Statement¶
A function with a non-empty result list must end in a terminating statement (typically return). A function with no results may omit return at the end. A bare return is allowed only when results are named (or when there are no results).
package main
func explicit() int {
return 42
}
func named() (n int) {
n = 42
return // naked return — uses named result
}
func void() {
// implicit return at end
}
func main() {
_ = explicit()
_ = named()
void()
}
3.7 Call Syntax¶
A function call evaluates the function expression, evaluates the arguments, and transfers control. Argument evaluation order is left to right, but evaluation of the function expression happens before the arguments. The result of a call expression is the function's result (or a comma-separated tuple, which can be assigned or used as the source of a multi-value expression).
package main
import "fmt"
func f() func(int) int {
fmt.Println("evaluating function expression")
return func(x int) int {
fmt.Println("body")
return x * 2
}
}
func arg() int {
fmt.Println("evaluating argument")
return 5
}
func main() {
result := f()(arg())
// Output:
// evaluating function expression
// evaluating argument
// body
fmt.Println(result) // 10
}
3.8 The main and init Functions Are Special¶
Two function names are reserved for special behavior at the package level:
func main()— must exist in packagemain. It is the entry point. It takes no arguments and returns no values.func init()— runs once per package, beforemain. A package may have multipleinitfunctions (even in the same file). They run in the order they are declared.
package main
import "fmt"
func init() {
fmt.Println("init runs first")
}
func main() {
fmt.Println("then main")
}
3.9 Functions Are Compared Only to nil¶
Function values are not comparable to each other. f == g is a compile error. Only f == nil and f != nil are valid comparisons.
package main
import "fmt"
func a() {}
func b() {}
func main() {
var f func()
fmt.Println(f == nil) // true
// fmt.Println(a == b) // compile error: invalid operation
_ = a
_ = b
}
4. Type Rules¶
4.1 Function Type Identity¶
Two function types are identical when they have the same parameter types in the same order, the same result types in the same order, and either both are variadic or neither is. Parameter and result names do not affect type identity.
package main
import "fmt"
type Op1 func(a, b int) int
type Op2 func(x, y int) int // identical to Op1 — names ignored
func main() {
var f Op1 = func(a, b int) int { return a + b }
var g Op2 = f // OK: same underlying type
fmt.Println(g(2, 3))
}
4.2 Assignability¶
A function value is assignable to a variable of function type when the underlying types match. There is no implicit conversion between different function types, even if the signatures differ only in parameter names.
package main
func add(a, b int) int { return a + b }
type Adder func(int, int) int
func main() {
var f Adder = add // OK
_ = f
}
4.3 The Zero Value of a Function Type¶
The zero value of a function type is nil. Calling a nil function panics with runtime error: invalid memory address or nil pointer dereference.
package main
func main() {
var f func()
// f() // panic: runtime error: invalid memory address or nil pointer dereference
_ = f
}
4.4 No Default Parameter Values¶
Go has no default parameter values. Every parameter must be passed at the call site. The idiomatic alternative is to overload via additional named functions or to use option-struct / functional-option patterns.
package main
import "fmt"
type ServerOpts struct {
Port int
TimeoutSeconds int
}
func startServer(opts ServerOpts) {
if opts.Port == 0 {
opts.Port = 8080 // caller-omitted field gets default here
}
if opts.TimeoutSeconds == 0 {
opts.TimeoutSeconds = 30
}
fmt.Printf("listening on :%d (timeout %ds)\n", opts.Port, opts.TimeoutSeconds)
}
func main() {
startServer(ServerOpts{}) // both defaults
startServer(ServerOpts{Port: 9000})
}
4.5 No Function Overloading¶
Two top-level functions in the same package may not share a name. Go has no function overloading by parameter list. Different behavior with different parameter shapes must be expressed via different names, variadic parameters, or generic type parameters.
package main
func add(a, b int) int { return a + b }
// func add(a, b float64) float64 { return a + b } // compile error: redeclared
func main() { _ = add(1, 2) }
4.6 Generic Functions (Go 1.18+)¶
A function may declare type parameters in [ ... ] between the name and the parameter list. The type parameters constrain a set of allowed argument types. Type parameters are part of the call site, not the function type for assignability purposes — once instantiated, the result is a concrete function value.
package main
import "fmt"
func Max[T int | float64 | string](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 7))
fmt.Println(Max("apple", "banana"))
fmt.Println(Max[float64](1.5, 2.5))
}
5. Behavioral Specification¶
5.1 Argument Passing Is Always By Value¶
Every argument in Go is passed by value. The function receives a copy of the argument. To mutate the caller's variable, pass a pointer.
package main
import "fmt"
func tryIncrement(x int) { x++ }
func actuallyIncrement(x *int) { *x++ }
func main() {
n := 10
tryIncrement(n)
fmt.Println(n) // 10 — unchanged
actuallyIncrement(&n)
fmt.Println(n) // 11
}
The argument-by-value rule applies even to slices, maps, and channels — what is copied is the header (slice descriptor, map handle, channel handle), not the underlying data. See 2.6.7 Call by Value and 2.7.3 With Maps & Slices.
5.2 Argument Evaluation Order¶
Arguments are evaluated left to right before the call. The function expression itself is evaluated before the arguments.
package main
import "fmt"
func chooseFn() func(int, int) int {
fmt.Println("1: function expression")
return func(a, b int) int { return a + b }
}
func arg(label string, v int) int {
fmt.Println("eval arg:", label)
return v
}
func main() {
r := chooseFn()(arg("first", 10), arg("second", 20))
fmt.Println("result:", r)
// Output:
// 1: function expression
// eval arg: first
// eval arg: second
// result: 30
}
5.3 Stack vs Heap Allocation¶
Where parameters live (stack vs heap) is determined by escape analysis, not by syntax. A parameter that does not escape its function is stored on the goroutine stack. A parameter whose address is taken and stored beyond the function's lifetime escapes to the heap. This is invisible to the program but observable via go build -gcflags="-m".
package main
func stackOnly(x int) int {
y := x + 1 // y stays on the stack
return y
}
var sink *int
func escapes(x int) {
y := x + 1
sink = &y // y escapes to the heap
}
func main() {
_ = stackOnly(1)
escapes(2)
}
Run go build -gcflags="-m=2" to see escape decisions.
5.4 Recursion¶
Functions may call themselves. The Go runtime grows the goroutine stack dynamically (starting at 2 KiB or 8 KiB depending on version), so deep recursion does not immediately overflow. There is no tail call optimization in the standard gc compiler.
package main
import "fmt"
func factorial(n uint64) uint64 {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func main() {
fmt.Println(factorial(20)) // 2432902008176640000
}
5.5 Deferred Calls Run on Function Exit¶
defer registers a call that runs when the surrounding function returns (whether by normal return, panic, or runtime.Goexit). Deferred calls run in LIFO order. They observe and may modify named return values.
package main
import "fmt"
func defers() (result int) {
defer func() { result *= 10 }()
defer fmt.Println("defer 2")
defer fmt.Println("defer 1")
result = 5
return
}
func main() {
fmt.Println("returned:", defers())
// Output:
// defer 1
// defer 2
// returned: 50
}
5.6 Panic Unwinds the Call Stack¶
A panic aborts the current function and unwinds, running deferred calls in each frame, until the goroutine ends or recover (called inside a deferred function) stops the unwind.
package main
import "fmt"
func mayPanic() {
panic("something broke")
}
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
mayPanic()
fmt.Println("never reached")
}
func main() {
safeCall()
fmt.Println("program continues")
}
6. Defined vs Undefined Behavior¶
| Situation | Behavior |
|---|---|
| Calling a nil function | Defined — runtime panic (nil pointer dereference) |
| Function returning before assigning a named result | Defined — returns the zero value of the result type |
| Recursive call deep enough to grow the stack | Defined — runtime grows the stack |
| Stack growth that exceeds the per-goroutine limit (default 1 GiB on 64-bit) | Defined — runtime fatal: "stack overflow" |
defer in a function that panics | Defined — deferred functions run during unwind |
defer of a nil function value | Defined — the panic is delayed until the deferred call would run |
| Returning more or fewer values than declared | Compile error |
| Calling a function with mismatched argument count or types | Compile error |
Comparing two function values with == (other than to nil) | Compile error |
Address of a function (&f) | Compile error — only addresses of variables are allowed; you can take the address of a function-typed variable instead |
| Function literal capturing a loop variable | Defined — Go ≥ 1.22 captures per-iteration; Go < 1.22 captures shared variable |
7. Edge Cases from Spec¶
7.1 Empty Parameter and Result Lists¶
A function with no parameters still needs (). A function with no result has no result list at all.
7.2 The Blank Identifier as a Parameter Name¶
A parameter name may be _ (the blank identifier). The argument is still required at the call site, but the value is unreferenced inside the function. This is useful for satisfying an interface signature without using the value.
package main
import "fmt"
func handler(_ int, name string) {
fmt.Println("name:", name)
}
func main() {
handler(99, "Ada") // 99 is ignored
}
7.3 Trailing Comma in Parameter List¶
Like all Go composite literals, parameter lists may end in a trailing comma when split across lines.
package main
func longSig(
a int,
b int,
c int, // trailing comma OK
) int {
return a + b + c
}
func main() { _ = longSig(1, 2, 3) }
7.4 Functions With No Body (Assembly)¶
A function declared without a body is implemented elsewhere (typically in assembly under cmd/compile's rules, or via //go:linkname). Such declarations are common in runtime and math packages.
// In math/sqrt_amd64.s, this function's body is provided by assembly.
// In Go source it is declared without a body:
//
// func archSqrt(x float64) float64
//
// Outside of low-level packages, never write bodyless functions.
7.5 The init Function May Appear Multiple Times¶
A package may declare multiple init functions, in any combination of files. They run in source order within a file and in import-graph order across files. init cannot be called explicitly by user code.
package main
import "fmt"
func init() { fmt.Println("init A") }
func init() { fmt.Println("init B") }
func main() { fmt.Println("main") }
7.6 Returning a Function¶
The result of a function may itself be a function value, enabling factory patterns.
package main
import "fmt"
func multiplier(factor int) func(int) int {
return func(x int) int { return x * factor }
}
func main() {
double := multiplier(2)
triple := multiplier(3)
fmt.Println(double(5), triple(5)) // 10 15
}
7.7 Methods Are Functions With a Receiver¶
A method declaration is the same syntax with an additional receiver list before the function name. From the spec's standpoint, a method is just a function whose first argument is the receiver. Method declarations are covered in chapter 3.
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func main() {
var c Counter
c.Inc()
c.Inc()
fmt.Println(c.n) // 2
}
7.8 Function-Type Parameter Name Reuse¶
Inside a func(...) type expression, names are optional. When omitted, the type lists must be unambiguous.
package main
type Handler func(int, int) bool // names omitted
type NamedHandler func(a, b int) bool // names present, no semantic difference
func main() {
var h Handler = func(a, b int) bool { return a == b }
var n NamedHandler = h
_ = n
}
8. Version History¶
| Go Version | Change |
|---|---|
| Go 1.0 | func declarations, function values, multiple return values, variadic functions, defer, panic/recover all present from launch. |
| Go 1.1 | Method values formalized: obj.M produces a bound function value. |
| Go 1.5 | Compiler rewritten in Go; no language changes to functions. |
| Go 1.17 | Register-based calling convention introduced for amd64 (function call ABI changed; observable in stack traces and benchmarks). Extended to arm64 in Go 1.18. |
| Go 1.18 | Generics: TypeParameters syntax func F[T constraint](x T) T added. |
| Go 1.21 | min, max, and clear become built-in functions. |
| Go 1.22 | Loop-variable per-iteration semantics affect closures returned from loops (see 2.6.5 Closures). |
| Go 1.23 | Range-over-function iterators: a function with signature func(yield func(V) bool) can be ranged over. |
9. Implementation-Specific Behavior¶
9.1 Calling Convention¶
Before Go 1.17 the standard gc compiler used a stack-based calling convention: arguments and results were passed on the goroutine stack. Since Go 1.17 (amd64) and Go 1.18 (arm64), Go uses a register-based calling convention with up to ~9 integer and ~15 floating-point argument registers. This typically produces 5–10% faster code in hot paths.
The convention is an implementation detail; you cannot rely on a specific register from Go source. CGO and assembly stubs use a documented bridging convention.
9.2 Inlining¶
The compiler inlines small functions automatically. Since Go 1.20 the inliner can inline functions with for loops, type switches, and method calls. Hints:
//go:inlineis not a recognized pragma in cmd/compile.//go:noinlineforces a function to never be inlined (used in benchmarks).- Use
go build -gcflags="-m"to see inlining decisions.
package main
//go:noinline
func notInlined(a, b int) int { return a + b }
func main() { _ = notInlined(1, 2) }
9.3 Stack Size¶
Each goroutine starts with a small stack (~2 KiB in modern Go). The runtime grows the stack as needed by copying it to a larger area. The hard limit per goroutine is set by runtime/debug.SetMaxStack (default 1 GiB on 64-bit systems). Exceeding the limit kills the program with runtime: goroutine stack exceeds limit.
9.4 No Tail Call Optimization¶
The standard gc compiler does not perform tail call optimization. Deeply recursive code in tail position grows the stack. For algorithms that require tail recursion (e.g., trampolined evaluators), rewrite as an explicit loop.
9.5 Linking and //go:linkname¶
The directive //go:linkname localname remotepkg.RemoteName lets a function declaration in one package be linked to another package's private symbol. This bypasses normal visibility rules and is restricted to packages that import unsafe. Use is reserved for runtime-internal purposes.
10. Spec Compliance Checklist¶
- Function declared with
func name(params) result { body } - Result list parenthesized when multiple results or any named results
- No nested named functions (only function literals inside other functions)
- Every parameter declared at call site (no defaults)
- No two top-level functions share a name in the same package
- Function values compared only to
nil -
func main()exists exactly once in packagemain -
initfunctions take no arguments and return no values -
returnprovided wherever a result list is non-empty (or function has a named result and ends in a terminating statement) - Argument count and types match the function signature
- Variadic parameter (
...T) appears only as the last parameter - Function literals capturing closures account for Go 1.22 loop-variable semantics
11. Official Examples¶
Example 1: All Function Declaration Forms¶
package main
import "fmt"
// 1. No params, no result
func ping() { fmt.Println("ping") }
// 2. Params with grouped types
func add(a, b int) int { return a + b }
// 3. Mixed param types
func tag(name string, count int) string {
return fmt.Sprintf("%s:%d", name, count)
}
// 4. Multiple results
func divmod(a, b int) (int, int) { return a / b, a % b }
// 5. Named results
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
// 6. Variadic
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 7. Function value as parameter
func apply(f func(int) int, x int) int { return f(x) }
// 8. Function value as result
func adder(by int) func(int) int {
return func(x int) int { return x + by }
}
func main() {
ping()
fmt.Println(add(3, 4))
fmt.Println(tag("retries", 5))
q, r := divmod(17, 5)
fmt.Println(q, r)
fmt.Println(split(100))
fmt.Println(sum(1, 2, 3, 4, 5))
fmt.Println(apply(func(x int) int { return x * x }, 6))
fmt.Println(adder(10)(5))
}
Expected output:
Example 2: Function as Map Value¶
package main
import "fmt"
func main() {
ops := map[string]func(int, int) int{
"+": func(a, b int) int { return a + b },
"-": func(a, b int) int { return a - b },
"*": func(a, b int) int { return a * b },
}
for _, op := range []string{"+", "-", "*"} {
fmt.Printf("3 %s 4 = %d\n", op, ops[op](3, 4))
}
}
Expected output:
Example 3: Why Argument Evaluation Order Matters¶
package main
import "fmt"
func main() {
i := 0
f := func(a, b int) {
fmt.Println("a:", a, "b:", b)
}
f(i, func() int { i++; return i }())
// a: 0 b: 1
// — left arg captured i==0 BEFORE the right arg incremented it.
}
Example 4: Invalid Constructs (Compile Errors)¶
// 1. Nested named function:
// func outer() {
// func inner() {} // ERROR: nested func not allowed
// }
// 2. Two functions with same name in package:
// func add(a, b int) int { return a + b }
// func add(a, b float64)... // ERROR: redeclared
// 3. Default parameter value:
// func greet(name string = "World") {} // ERROR: unexpected '='
// 4. Function comparison:
// func a() {}
// func b() {}
// _ = a == b // ERROR: invalid operation
// 5. Address-of a function:
// _ = &fmt.Println // ERROR: cannot take address of fmt.Println
// 6. Variadic param not last:
// func bad(args ...int, x int) {} // ERROR: can only use ... as final argument
12. Related Spec Sections¶
| Section | URL | Relevance |
|---|---|---|
| Function declarations | https://go.dev/ref/spec#Function_declarations | Primary specification |
| Function types | https://go.dev/ref/spec#Function_types | Type identity, function-typed variables |
| Function literals | https://go.dev/ref/spec#Function_literals | Anonymous functions (covered in 2.6.4) |
| Calls | https://go.dev/ref/spec#Calls | Call semantics, argument evaluation |
| Return statements | https://go.dev/ref/spec#Return_statements | When return is required, naked return |
| Defer statements | https://go.dev/ref/spec#Defer_statements | Deferred function calls |
| Handling panics | https://go.dev/ref/spec#Handling_panics | panic and recover |
| Type parameter declarations | https://go.dev/ref/spec#Type_parameter_declarations | Generic functions (Go 1.18+) |
| Method declarations | https://go.dev/ref/spec#Method_declarations | Functions with a receiver |
| Program initialization | https://go.dev/ref/spec#Program_initialization | init and main semantics |