Functions
In Go, functions are first-class citizens. Functions are the most fundamental building blocks and the core of Go.
Declaration
The format for declaring a function is as follows:
func functionName([parameter list]) [return value] {
function body
}There are two ways to declare a function. One is to directly declare using the func keyword, and the other is to declare using the var keyword:
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}A function signature consists of the function name, parameter list, and return value. Here is a complete example. The function name is Sum, it has two int type parameters a and b, and the return value type is int.
func Sum(a int, b int) int {
return a + b
}One very important point is that Go does not support function overloading. The following code cannot be compiled:
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}Go's philosophy is: if the signature is different, then it's a completely different function, so it shouldn't have the same name. Function overloading makes code confusing and hard to understand. Whether this philosophy is correct is a matter of opinion. At least in Go, you can know what a function does just by its name without having to find which overload it is.
Parameters
Parameter names in Go can be without names. Generally, this is used in interface or function type declarations, but for readability, it's still recommended to give parameters names:
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}For parameters of the same type, you only need to declare the type once, but the condition is that they must be adjacent:
func Log(format string, a1, a2 any) {
...
}Variadic parameters can receive 0 or more values and must be declared at the end of the parameter list. The most typical example is the fmt.Printf function.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}It's worth mentioning that function parameters in Go are passed by value, meaning the value of the argument is copied when passed. If you're worried about copying a lot of memory when passing slices or maps, you don't need to worry, because these two data structures are essentially pointers.
Return Values
Here is a simple example of function return values. The Sum function returns an int value.
func Sum(a, b int) int {
return a + b
}When a function has no return value, you don't need void, just omit the return value.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go allows functions to have multiple return values. In this case, you need to enclose the return values in parentheses.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 cannot be used as dividend")
}
return a / b, nil
}Go also supports named return values. They cannot duplicate parameter names. When using named return values, the return keyword doesn't need to specify which values to return.
func Sum(a, b int) (ans int) {
ans = a + b
return
}Like parameters, when there are multiple named return values of the same type, you can omit the repeated type declaration:
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}No matter how named return values are declared, the values after the return keyword always have the highest priority.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d will not be returned
return a + b, a * b
}Anonymous Functions
An anonymous function is a function without a name. For example, the function func(a, b int) int below has no name, so we can only call it by immediately following its function body with parentheses.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}When calling a function and its parameter is a function type, the name no longer matters. You can directly pass an anonymous function:
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}This is an example of custom sorting rules. slices.SortFunc accepts two parameters: one is the slice, and the other is the comparison function. If reuse is not considered, we can directly pass an anonymous function.
Closures
The concept of Closure, also known as Lambda expression in some languages, is used together with anonymous functions. Closure = function + environment reference. Let's look at an example:
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}Output:
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512The return value of the Exp function is a function, which we call grow function here. Each time it's called, the variable e grows exponentially. The grow function references two variables from the Exp function: e and n. They are created within the scope of the Exp function. Normally, as the Exp function finishes, these variables' memory would be recovered as they go out of scope. However, because the grow function references them, they cannot be recovered and escape to the heap. Even though the Exp function's lifecycle has ended, the lifecycle of variables e and n has not ended. They can still be directly modified within the grow function. The grow function is a closure function.
Using closures, you can easily implement a function to calculate the Fibonacci sequence:
func main() {
// 10 Fibonacci numbers
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}Output:
0
1
1
2
3
5
8
13
21
34Deferred Calls
The defer keyword can make a function call delayed. These deferred functions will all be executed one by one before the function returns. Let's look at an example:
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Output:
2
1Because defer is executed before the function returns, you can also modify the function's return value in defer:
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}When there are multiple deferred functions, they are executed in a last-in-first-out (stack) order:
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1Deferred calls are commonly used for releasing file resources, closing network connections, etc. Another use is to catch panic, but this is covered in the error handling section.
Loops
Although not explicitly prohibited, it's generally recommended not to use defer in for loops:
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}Output:
4
3
2
1
0The result of this code is correct, but the process may not be. In Go, each defer created requires allocating a memory space in the current goroutine. Suppose in the above example, instead of a simple for n loop, it's a more complex data processing flow. When external requests suddenly surge, a large number of defers will be created in a short time. When the loop count is large or uncertain, it may cause sudden memory spikes. This is generally called a memory leak.
Pre-computation of Parameters
There are some counter-intuitive details about deferred calls. For example:
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}This pitfall is quite hidden. The author once spent half a day debugging because of this issue. Can you guess what the output is?
2
3
1Many people think the output would be:
3
2
1Based on the user's intention, fmt.Println(Fn1()) should be executed after the function body finishes. fmt.Println does execute last, but Fn1() is unexpected. The next example makes this more obvious:
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}Its output is definitely 3, not 7. If using a closure instead of a deferred call, the result is different:
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}The closure outputs 7. What if we combine deferred call and closure?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}This time it's normal, it outputs 7. Let's change it again, no closure this time:
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}The output is back to 3. By comparing the examples above, we can see that:
defer fmt.Println(sum(a,b))is actually equivalent to:
defer fmt.Println(3)Go doesn't wait until the end to call the sum function. The sum function is called before the deferred call is executed, and the result is passed as a parameter to fmt.Println. In summary, for functions directly affected by defer, their parameters are pre-computed. This causes the strange phenomenon in the first example. Special attention needs to be paid to this situation, especially when using function return values as parameters in deferred calls.
