Deklarowanie funkcji
Deklaracja funkcji to func, nazwa, parametry, typ wyniku i ciało. Typ zapisuje się po nazwie parametru, a nie przed nią.
add przyjmuje dwie wartości int i zwraca int. greet nic nie zwraca, więc jej sygnatura nie ma typu wyniku, a ciało nie potrzebuje return.
Funkcja, która deklaruje wynik, musi kończyć się instrukcją kończącą. Jeśli jakaś ścieżka może dojść do zamykającego nawiasu klamrowego bez zwrócenia wartości, kompilator zatrzymuje się z błędem missing return.
Parametry o wspólnym typie
Kolejne parametry tego samego typu mogą dzielić jedną nazwę typu. func add(a, b int) int znaczy dokładnie to samo co func add(a int, b int) int. Można też mieszać:
func scale(x, y float64, factor int) (float64, float64)
Tutaj x i y są typu float64, a factor to int.
Wartości zwracane
Funkcja może zwrócić zero, jedną lub kilka wartości. Kilka wyników zapisuje się w nawiasach, a wywołujący odbiera je przypisaniem wielokrotnym:
To wypisuje 3 2. Para (value, error) to najczęstsze zastosowanie tej funkcji języka w Go i ma własną stronę o wielu wartościach zwracanych.
Argumenty są kopiowane
Go przekazuje każdy argument przez wartość. Funkcja pracuje na kopii, więc przypisanie do parametru nigdy nie zmienia zmiennej wywołującego.
Wynik:
{1 2}
{100 2}
[99 2 3]
Trzecia linia zaskakuje wiele osób. Wartość slice'a to mały nagłówek (wskaźnik, długość, pojemność). Kopia wskazuje na tę samą tablicę bazową, więc zapis s[0] zmienia element wywołującego. Ale append wewnątrz funkcji może zaalokować nową tablicę, a wywołujący nigdy nie zobaczy tego nowego nagłówka. Jeśli funkcja powiększa slice, zwróć nowy slice. Ta sama logika dotyczy map: funkcja może dodać klucze, które zobaczy wywołujący, bo wartość mapy odwołuje się do wspólnych danych.
Użyj parametru wskaźnikowego, gdy funkcja musi zmienić zmienną wywołującego albo gdy wartość to duża struktura, której nie chcesz kopiować przy każdym wywołaniu.
Funkcje są wartościami
Funkcja ma typ zapisywany bez nazwy: func(int, int) int. Funkcję możesz zapisać w zmiennej, umieścić w mapie lub slice'ie i przekazać do innej funkcji.
square to funkcja anonimowa przypisana do zmiennej. Funkcje anonimowe mogą też czytać i zmieniać zmienne z otaczającego zakresu, co czyni z nich domknięcia.
Nazwany typ funkcyjny ułatwia czytanie sygnatur, gdy ten sam kształt pojawia się w wielu miejscach:
type Transform func(int) int
func apply(xs []int, f Transform) []int
Wartość zerowa typu funkcyjnego to nil. Wywołanie funkcji nil kończy się panic z komunikatem invalid memory address or nil pointer dereference, więc sprawdzaj opcjonalne callbacki przed ich wywołaniem. Wartości funkcyjne można porównywać tylko z nil, nigdy ze sobą nawzajem.
Rekurencja
Funkcja może wywoływać samą siebie. Go nie ma optymalizacji wywołań ogonowych, ale stosy gorutyn rosną w miarę potrzeb, więc zwykłe głębokości rekurencji nie są problemem.
for i := range 6 liczy od 0 do 5 i wymaga Go 1.22 lub nowszego.
Czego nie mają funkcje w Go
- Brak przeciążania. Dwie funkcje w jednym pakiecie nie mogą mieć tej samej nazwy, nawet z innymi parametrami.
- Brak argumentów domyślnych i nazwanych. Każdy parametr jest przekazywany przy każdym wywołaniu, w kolejności. Struktura konfiguracji daje nazwane, opcjonalne pola:
NewServer(Config{Port: 8080}). - Brak zagnieżdżonych funkcji nazwanych. W ciele funkcji możesz deklarować tylko funkcje anonimowe, zwykle przypisane do zmiennej.
Listy argumentów o zmiennej długości są obsługiwane przez funkcje wariadyczne.
Funkcje eksportowane i nieeksportowane
Funkcja, której nazwa zaczyna się wielką literą (Parse), jest eksportowana i można ją wywołać z innych pakietów. Nazwa zaczynająca się małą literą (parse) jest widoczna tylko w jej własnym pakiecie. Nie ma słów kluczowych public ani private; cała reguła to wielkość pierwszej litery.
Typowe błędy
- Oczekiwanie, że zmiana parametru dotrze do wywołującego. Przypisanie do parametru zmienia tylko kopię. Zwróć nową wartość albo przyjmij wskaźnik.
- Dopisywanie w funkcji i ignorowanie wyniku.
func add(s []int) { s = append(s, 1) }nie wpływa na długość slice'a wywołującego. Napiszfunc add(s []int) []inti użyjs = add(s). - Nieużyte wyniki. Go pozwala zignorować wartość zwracaną.
go vetnie zgłasza porzuconego błędu, ale lintery takie jakerrcheck(częśćgolangci-lint) tak. Używaj_ =tylko wtedy, gdy ignorowanie to świadoma decyzja.
Najczęściej zadawane pytania
Jak zdefiniować funkcję w Go?
Użyj słowa kluczowego func, nazwy, listy parametrów z typami po nazwach i typu wyniku: func add(a, b int) int { return a + b }. Funkcja bez wyniku pomija typ, a funkcja z kilkoma wynikami wymienia je w nawiasach: func divmod(a, b int) (int, int).
Czy Go przekazuje argumenty przez wartość, czy przez referencję?
Zawsze przez wartość. Funkcja dostaje kopię każdego argumentu. Dla int czy struktury ta kopia jest niezależna od zmiennej wywołującego. Slice'y, mapy, kanały i wskaźniki to małe wartości, które odwołują się do wspólnych danych, więc zmiany elementów, na które wskazują, są widoczne dla wywołującego, a przypisanie nowej wartości do samego parametru już nie. Przekaż wskaźnik (*T), gdy funkcja musi zmienić zmienną wywołującego.
Czy w Go można przekazać funkcję jako parametr?
Tak. Funkcje to wartości z typem, na przykład func(int) int. Zadeklaruj parametr tego typu i przekaż dowolną funkcję o pasującej sygnaturze, nazwaną lub anonimową: func apply(xs []int, f func(int) int).
Czy Go obsługuje przeciążanie funkcji lub parametry domyślne?
Nie. Każda nazwa funkcji w pakiecie musi być unikalna, a każdy parametr trzeba przekazać przy każdym wywołaniu. Typowe zamienniki to osobne nazwy (NewServer, NewServerWithTLS), struktura konfiguracji, parametry wariadyczne albo opcje funkcyjne.