04. Functions

Functions

  • Функции в Go в целом ведут себя как и в других языках.

  • В Go функции глобальны в пределах пакета.

  • В Go не работает перегрузка функций - сигнатура функции не является её уникальным идентификатором. Частично перегрузка работает только для методов.

  • Go does let you implicitly ignore all of the return values for a function.

Упрощенная запись для нескольких значений одинакового типа:

func sumLight(i, j int) int {
    return i + j
}

Multiple return values

A function can return any number of results:

func swap(x, y string) (string, string) {
    return y, x
}

func main() {
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}

Named return values

Go's return values may be named. If so, they are treated as variables defined at the top of the function. A return statement without arguments returns the named return values. This is known as a "naked" return. Naked return statements should be used only in short functions, as with the example shown here. They can harm readability in longer functions.

import "fmt"

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
    fmt.Println(split(17)) // 7 10
}

Nested function calls

Результат выполнения функции можно передать в другую функцию, даже если там много агрументов:

package main

import (
	"fmt"
)

func f1() (float64, float64) {return 1, 2}
func f2(float64, float64) {}

f2(f1()) // OK

Passing arguments

Everything in go passed by value.

Where to use struct values and where to use pointer values as function arguments and return types?

Most of the time, you should use a value. They make it easier to understand how and when your data is modified. A secondary benefit is that using values reduces the amount of work that the garbage collector has to do.

  • The time to pass a pointer into a function is constant for all data sizes, roughly one nanosecond. This makes sense, as the size of a pointer is the same for all data types. Passing a value into a function takes longer as the data gets larger. It takes about a millisecond once the value gets to be around 10 megabytes of data.

  • For data structures that are smaller than a megabyte, it is actually slower to return a pointer type than a value type. For example, a 100-byte data structure takes around 10 nanoseconds to be returned, but a pointer to that data structure takes about 30 nanoseconds. Once your data structures are larger than a megabyte, the performance advantage flips. It takes nearly 2 milliseconds to return 10 megabytes of data, but a little more than half a millisecond to return a pointer to it.

Go любит размещать структуры на стеке и поэтому ему удобнее работать со структурами а не с указателями. Указатели указывают на память в куче и ее размеры неведомы и ее нельзя разложить на стеке.

Pass by pointer, return by value

In order for Go to allocate the data the pointer points to on the stack, several conditions must be true. It must be a local variable whose data size is known at compile time. When the compiler determines that the data can’t be stored on the stack, we say that the data the pointer points to escapes the stack and the compiler stores the data on the heap.

You might be wondering: what’s so bad about storing things on the heap? There are two problems related to performance.

  • First is that the garbage collector takes time to do its work. It isn’t trivial to keep track of all of the available chunks of free memory on the heap or tracking which used blocks of memory still have valid pointers.

  • The second problem deals with the nature of computer hardware. RAM might mean “random access memory,” but the fastest way to read from memory is to read it sequentially. A slice of structs in Go has all of the data laid out sequentially in memory. This makes it fast to load and fast to process. A slice of pointers to structs (or structs whose fields are pointers) has its data scattered across RAM, making it far slower to read and process.

Pointers indicate mutability

Rather than declare that some variables and parameters are immutable, Go developers use pointers to indicate that a parameter is mutable.

  • Since Go is a call by value language, the values passed to functions are copies. For non-pointer - types like primitives, structs, and arrays, this means that the called function cannot modify the original. Since the called function has a copy of the original data, the immutability of the original data is guaranteed.

  • However, if a pointer is passed to a function, the function gets a copy of the pointer. This still points to the original data, which means that the original data can be modified by the called function.

if you want the value assigned to a pointer parameter to still be there when you exit the function, you must dereference the pointer and set the value. Dereferencing puts the new value in the memory location pointed to by both the original and the copy.

func update(px *int) { 
    *px=20 // dereference
}
func main() { 
    x:=10
    update(&x)
    fmt.Println(x) // prints 20
}

Variadic function

Variadic functions can be called with any number of trailing arguments.

  • When passing arguments

  • For example, fmt.Println is a common variadic function.

  • Go in case of slice when passed to a variadic function using unpack operator, will use underneath array to build new slice. При передаче слайса в variadic функцию считай, что передается слайс по ссылке и любые модификации над аргументами массива параметров внутри функции приведут к изменению слайса.

ints as arguments.
func sum(nums ...int) {
    fmt.Print(nums, " ")
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}

func main() {
    sum(1, 2)
    sum(1, 2, 3)
    nums := []int{1, 2, 3, 4}
    sum(nums...)
}

Function closures

Go supports anonymous functions, which can form closures:

func intSeq() func() int {
    i := 0
    return func() int {
        i += 1
        return i
    }
}

func main() {
    nextInt := intSeq()

    fmt.Println(nextInt()) // 1
    fmt.Println(nextInt()) // 2
    fmt.Println(nextInt()) // 3

    newInts := intSeq()
    fmt.Println(newInts()) // 1
}

Closures and goroutines

Не используйте замыкания вместе с goroutines:

for _, val := range boo {
    go func() {
        hello(val)
    }()
}

Горутины выполняются асинхронно, поэтому замыкают контекст переменной после того как цикл отработал, и поэтому val в замыкании примет последнее значение цикла.

Defer

  • A defer statement defers the execution of a function until the surrounding function returns.

  • Значением defer может быть любое выражение (!), которое выполнится отложено.

  • Значением defer может быть анонимная функция defer func() { ... }()

  • The code within defer closures runs after the return statement.

  • The deferred call’s arguments are evaluated immediately, even though the function call is not executed until the surrounding function returns.

  • Just as defer doesn’t run immediately, any variables passed into a deferred closure aren’t evaluated until the closure runs.

  • defer функции могут менять возвращаемое значение функции, но у меня получилось это сделать с named return values, но не с локальными переменными.

func main() {
    defer fmt.Println("world")
    fmt.Println("hello")
}
// hello
// world

Deferred function calls are pushed onto a stack. Defer функция замыкает значения переданных ей аргументов на момент своего вызова. Если значение переменной поменялось далее, это не скажется на defer функции.

When a function returns, its deferred calls are executed in last-in-first-out order.

func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}
// counting,  done, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

Deferred Function Calls Can Modify the Named Return Results of Nesting Functions

func Triple(n int) (r int) {
	defer func() {
		r += n // modify the return value
	}()

	return n + n // <=> r = n + n; return
}

func main() {
	fmt.Println(Triple(5)) // 15
}

A very large deferred call stack may also consume much memory, and the unexecuted deferred calls may prevent some resources from being released in time. For example, if there are many files needed to be handled in a call to the following function, then a large number of file handlers will be not get released before the function exits.

func writeManyFiles(files []File) error {
	for _, file := range files {
		f, err := os.Open(file.path)
		if err != nil {
			return err
		}
		defer f.Close()

		_, err = f.WriteString(file.content)
		if err != nil {
			return err
		}

		err = f.Sync()
		if err != nil {
			return err
		}
	}

	return nil
}

For such cases, we can use an anonymous function to enclose the deferred calls so that the deferred function calls will get executed earlier.

First class citizen

Functions are values too:

  • They can be passed around just like other values.

  • Function values may be used as function arguments and return values.

  • Go functions may be closures.

import (
    "fmt"
    "time"
)

func getTimer() func() {
    start := time.Now()
    return func() {
        fmt.Println("Time since", time.Since(start))
    }
}

func main() {
    timer := getTimer()
    fmt.Println("Hello, playground")
    time.Sleep(100 * time.Millisecond)
    defer timer()
}

Last updated