Golang functions: syntax, parameters, returns, methods, closures, and defer

Tech reviewed: Deepak Prasad
Golang functions: syntax, parameters, returns, methods, closures, and defer

This page is a structured tour of functions in Go for readers who already know another language and want the full picture in one place: syntax, naming, how data goes in and out, value versus pointer semantics, methods, function values, closures, recursion, and defer. Official language material lives in A Tour of Go: Functions, the language specification, and Effective Go — Functions. Deeper follow-ups on this site include multiple return values, errors, variadic functions, the defer keyword, functions versus methods, goroutine return patterns, and two-parameter typing.

Every main example below was run with Go 1.24 on Linux unless noted.


Golang function overview for beginners

A function groups statements, gives them a name, and defines what goes in (parameters) and what comes out (return values). Benefits you use from day one:

  • Reuse logic without copy-paste.
  • Test units of behavior in isolation.
  • Express APIs: small exported functions and methods are the main way Go libraries compose.

The rest of this article follows a typical learning path: syntax and naming, declaring and calling, returns, argument passing, variadic and composite types, methods, anonymous functions and closures, higher-order patterns, recursion, then defer.


Golang function syntax

The general form of a package-level function is:

text
func name(parameterList) resultList {
    // body
}

resultList is either a single type (int) or a parenthesized list ((string, int, error)). The caller invokes name(arguments). Go does not use parentheses around the control expression in if / for / switch, but the parameter and result lists in a func always use parentheses as shown in the spec.

go
package main

import "fmt"

func sayHello() {
	fmt.Println("Hello, world!")
}

func greet(name string, age int) string {
	return fmt.Sprintf("Hello %s, you are %d years old.", name, age)
}

func main() {
	sayHello()
	fmt.Println(greet("John Doe", 32))
}
Output

Running the program prints Hello, world! and Hello John Doe, you are 32 years old.

main with signature func main() is the entry point for command-line programs in package main.


Naming convention to declare a golang function

Go uses letter case for export: names starting with an upper-case letter (Parse, HTTPClient) are visible outside the package; lower-case names (parse, internal) are package-private. That rule applies to functions, methods, struct fields, and type names. Package names stay short, lower-case, single word when possible (Effective Go — Names). For getters, the Go style is Owner() rather than GetOwner() when the meaning is still clear.


Declaring and calling functions

You declare a function once at package level (or as a nested function literal, covered later). You call it from the same package with name(args) or from another import path as otherpkg.Name(args) when exported.

Functions in the same file can call each other regardless of declaration order. There is no separate prototype syntax: the compiler reads the full file.


Returning data in a golang function

Returning a single value

Use return followed by an expression that matches the declared result type.

go
package main

import "fmt"

func square(x int) int {
	return x * x
}

func main() {
	fmt.Println(square(7))
}
Output

You should see 49.

Returning multiple values

Parenthesize two or more result types. Callers often use short variable declaration a, b := f().

go
package main

import "fmt"

func getUser() (string, int) {
	return "JohnDoe", 34
}

func main() {
	name, age := getUser()
	fmt.Println("Username:", name, "Age:", age)
	onlyName, _ := getUser()
	fmt.Println("ignore age:", onlyName)
}
Output

See golang return multiple values for named results, naked return, and ignoring values with _.

Returning an error

The idiomatic pattern for fallible work is (T, error): compute a value or return a zero value plus a non-nil error.

go
package main

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

func parsePositive(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("parse: %w", err)
	}
	if n <= 0 {
		return 0, errors.New("must be positive")
	}
	return n, nil
}

func main() {
	fmt.Println(parsePositive("42"))
	fmt.Println(parsePositive("-1"))
}
Output

You should see (42 <nil>) then (0 must be positive) (or similar wrapped parse error for non-numeric input). Always check err before using other results. More patterns live under golang return error.

Returning a pointer

Returning *T lets callers share one mutable value or avoid copying a large struct. It is safe to return the address of a local variable: the compiler allocates it on the heap when it must escape (see go build -gcflags=-m if you are curious).

go
package main

import "fmt"

type Point struct{ X, Y int }

func NewPoint(x, y int) *Point {
	return &Point{X: x, Y: y}
}

func main() {
	p := NewPoint(3, 4)
	fmt.Println(p.X + p.Y)
}
Output

You should see 7.

Named result parameters

You may name results and use bare return inside the body. This is optional sugar; use it when names improve documentation, not when they obscure control flow.

go
package main

import (
	"errors"
	"fmt"
)

func divide(a, b float64) (q float64, err error) {
	if b == 0 {
		err = errors.New("zero divisor")
		return
	}
	q = a / b
	return
}

func main() {
	fmt.Println(divide(10, 2))
	fmt.Println(divide(10, 0))
}
Output

You should see 5 <nil> then 0 zero divisor.


Passing arguments to a golang function

Go passes every argument by value: the function receives a copy of what the caller passed. For pointer types, the copied value is the address, so both caller and callee share the same pointed-to memory.

Arguments passed by value

Primitives (int, float64, string, small structs) are independent copies; mutating a parameter does not change the caller’s variable.

go
package main

import "fmt"

func byValue(name string) {
	name = "changed"
}

func main() {
	s := "original"
	byValue(s)
	fmt.Println("after byValue:", s)
}
Output

You should see after byValue: original.

Arguments passed by reference (pointers)

Pass &x when the callee should update x in the caller, or when copying a large struct is wasteful.

go
package main

import "fmt"

func byPointer(name *string) {
	*name = "changed"
}

func main() {
	s := "original"
	byPointer(&s)
	fmt.Println("after byPointer:", s)
}
Output

You should see after byPointer: changed.

Struct, map, slice, and channel parameters

Composite types still follow pass-by-value of the value’s header or descriptor.

Structs: copying the whole struct means field writes on the parameter do not affect the caller unless you pass *struct or return the updated struct.

go
package main

import "fmt"

type counter struct{ n int }

func bumpCopy(c counter) { c.n++ }
func bumpPtr(c *counter) { c.n++ }

func main() {
	x := counter{1}
	bumpCopy(x)
	fmt.Println("after copy:", x.n)
	bumpPtr(&x)
	fmt.Println("after ptr:", x.n)
}
Output

You should see after copy: 1 then after ptr: 2.

Maps and channels: the callee receives a copy of the map or channel descriptor, but it still refers to the same runtime map or channel, so mutations are visible to the caller.

go
package main

import "fmt"

func setKey(m map[string]int) { m["x"] = 42 }

func main() {
	m := map[string]int{}
	setKey(m)
	fmt.Println(m["x"])
}
Output

You should see 42.

Slices: the slice header (pointer, length, capacity) is copied; indexing or mutating elements through that header affects the shared backing array. Reassigning the slice variable inside the callee does not replace the caller’s slice header unless you pass *[]T or return the new slice.


Variadic input parameters and slices

A variadic parameter uses ...T and must be the last parameter; inside the function it behaves as []T. Callers can pass a, b, c or slice....

go
package main

import "fmt"

func printAll(items ...string) {
	for _, item := range items {
		fmt.Println(item)
	}
}

func main() {
	printAll("Go", "Docker", "Linux")
}
Output

For unpacking slices, mixing with fixed parameters, and pitfalls like nil versus empty variadic arguments, see variadic functions in Go.


Go methods

A method declares a receiver before the function name. Receivers may be value T or pointer *T.

go
package main

import "fmt"

type User struct {
	username string
	password string
}

func (u *User) setPassword(p string) {
	u.password = p
}

func (u User) summary() string {
	return fmt.Sprintf("user=%s", u.username)
}

func main() {
	u := User{username: "jdoe", password: "old"}
	fmt.Println(u.summary())
	u.setPassword("new")
	fmt.Println("password:", u.password)
}
Output

Pointer receivers are required when the method mutates the receiver or when you want to avoid copying large structs. Value receivers are fine for small immutable views. More trade-offs sit in functions versus methods and A Tour of Go: Methods.


Functions versus methods

Idea Package-level function Method
Syntax func Name(...) func (r T) Name(...)
Call site Name(args) value.Name(args)
Typical use Algorithms that do not need a receiver state Behavior tied to a type’s fields or invariants

Use functions for pure utilities; use methods when the first argument would always be the same conceptual “subject” of the operation.


Anonymous functions

A function literal has no name at package level; you assign it, pass it, or invoke it immediately.

go
package main

import "fmt"

func main() {
	double := func(x int) int { return x * 2 }
	fmt.Println(double(21))

	func(msg string) {
		fmt.Println(msg)
	}("inline IIFE")
}
Output

You should see 42 then inline IIFE.


Go closure function

A closure is a function value that references variables from an enclosing lexical scope. Those variables stay alive as long as the closure does.

go
package main

import "fmt"

func makeAdder(base int) func(int) int {
	return func(x int) int {
		return base + x
	}
}

func main() {
	addFive := makeAdder(5)
	fmt.Println(addFive(10))
}
Output

You should see 15.

When a closure is created inside a loop, capture the loop variable explicitly if you need per-iteration binding (historically i := i inside the loop body; Go 1.22+ changed for loop variable semantics so each iteration gets its own i for range and three-clause for, but explicit capture still reads clearly in complex loops).

go
package main

import "fmt"

func main() {
	var fs []func()
	for i := 0; i < 3; i++ {
		i := i
		fs = append(fs, func() { fmt.Println(i) })
	}
	for _, f := range fs {
		f()
	}
}
Output

You should see 0, 1, and 2 on separate lines.


Passing functions as parameters

Declare a parameter with the function type you need; callers pass nil only if the API allows it (rare).

go
package main

import (
	"fmt"
	"sort"
)

func main() {
	nums := []int{3, 1, 4}
	sort.Slice(nums, func(i, j int) bool {
		return nums[i] < nums[j]
	})
	fmt.Println(nums)
}
Output

You should see [1 3 4]. The net/http package uses related patterns (HandlerFunc) to adapt functions into interfaces.


Returning functions from functions

Higher-order functions return customized behavior—factories, decorators, or scoped configuration.

go
package main

import "fmt"

func withPrefix(prefix string) func(string) string {
	return func(s string) string {
		return prefix + ": " + s
	}
}

func main() {
	log := withPrefix("app")
	fmt.Println(log("started"))
}
Output

You should see app: started.


Recursive functions

Go supports recursion like C; each call allocates a new stack frame, so deep recursion can overflow the stack for pathological inputs. Always include a base case.

go
package main

import "fmt"

func fact(n int) int {
	if n <= 1 {
		return 1
	}
	return n * fact(n-1)
}

func main() {
	fmt.Println(fact(5))
}
Output

You should see 120.


Using the defer keyword

defer schedules a call to run when the surrounding function returns, after any return values are evaluated but before control returns to the caller. Deferred calls run in last-in-first-out order.

go
package main

import "fmt"

func main() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
}
Output

You should see 3, then 2, then 1 on separate lines—the deferred prints run after the body finishes, newest defer first.

Typical production uses: defer f.Close() after a successful os.Open, defer mu.Unlock() after Lock, or pairing defer with recover in server handlers (use sparingly). A full treatment lives in the defer keyword guide and the spec: defer statements.


Summary

Go functions are declared with func, optional receiver, name, parameters, results, and body. Data comes back as single or multiple return values; errors are ordinary return values. Arguments are always passed by value, so pointers, maps, channels, and slice headers explain shared mutation. Variadic parameters are summarized here and expanded in the variadic article. Methods attach behavior to types; function literals and closures capture environment; higher-order APIs accept and return functions. Recursion needs a base case; defer orders cleanup on return. Together these topics match how the language and libraries express behavior—see the official links below when you need normative wording.


References


Frequently Asked Questions

1. What is golang function syntax?

A declaration starts with func, an optional receiver for methods, the name (optional for function literals), the parameter list in parentheses, an optional result list, and a body in braces; for example func(name string, age int) (string, error).

2. How do golang multiple types appear in one function?

Use several parameters with different types, or several return values written as a parenthesized list like (int, error); callers assign with a, b := f() or ignore with _.

3. How do I golang pass function as parameter?

Declare a parameter with a function type such as func(int, int) bool and pass a matching function value or literal; the standard library uses this pattern in sort.Slice, HTTP handlers, and many callbacks.

4. What is a golang function type?

It is the signature without a name, for example func(int) int; values of that type are callable and can be stored in variables or passed like any other value.

5. What is the difference between a function and a method in Go?

A method declares a receiver before the function name, func (r *T) Name(), and is called on a value of that type with dot syntax; a plain package-level function has no receiver.

6. If maps are passed by value, why does a function change the caller map?

The map value is a small runtime descriptor copied by value, but it still refers to the same underlying hash table, so updates through the parameter are visible to the caller; channels behave similarly.

7. What does defer do in Go?

defer schedules a function call to run when the surrounding function returns, in last-in-first-out order; it is commonly paired with Close on successful Open calls. See the defer guide linked from this article.

8. When should I use a pointer receiver on a method?

Use *T when the method must mutate the receiver, when the struct is large and copying is costly, or when you need consistency if some methods mutate; use value T for small immutable data or when you must protect copies.
Antony Shikubu

Systems Integration Engineer

Highly skilled software developer with expertise in Python, Golang, and AWS cloud services.

  • Mastering Ansible Automation
  • Docker and Kubernetes: The Complete Guide
  • Go: Data Structures, Algorithms and Design Patterns With Go
  • Go (programming language)
  • Python (programming language)
  • Amazon Web Services