Funkcje

Funkcje w Go występują w różnych rolach:

  • podstawowej, jako podprogram - wydzielona część programu
  • clousures, czyli domknięcia
  • metody nazwanego typu (najczęściej struktur)
  • goroutines, czyli równoległe wywołanie

O funkcjach jako metodach i goroutines możesz przeczytać w innych artykułach (o obiektowości i współbieżności).

Funkcja jako nazwana część programu

Podstawowa definicja funkcji w Go rozpoczyna się od słowa func po czym następuje jej nazwa i sygnatura. Sygnatura to w ogólności lista przyjmowanych argumentów i wartości zwracanych z funkcji. W szczególności obie listy mogą być puste:

func empty() {
}

Niemniej takie "puste" funkcje, rzadko występują w normalnym programowaniu, wyjątkami są tzw. domknięcia (clousures), o których przeczytasz później.

Argumenty funkcji

Zwykle chcemy przekazać do funkcji jakiś argument. Lista argumentów może być zadeklarowana na kilka różnych sposobów

Jako pary identyfikator-typ rozdzielone przecinkiem:

func printTwoIntArguments (a int, b int) {
    fmt.Println(a, b)
}

Ale gdy jak w powyższym przykładzie kilka argumentów jest tego samego typu, możemy zgrupować identyfikatory a ich typ umieścić po nich:

func printTwoIntArguments (a, b int) {
    fmt.Println(a, b)
}

Jest jeszcze jedna metoda ustalania argumentów, mało popularna bo ma mało zastosowań: możemy wymienić same typy. Jedyne sensowne wykorzystanie takiej cechy języka widzę w przypadku potrzeby spełnienia konkretnego interfejsu (ale o interfejsach gdzie indziej).

func dummySignature(int, int) {
    fmt.Println("Inside dummySignature func")
}

... (dotdotdot)

Istnieje specjalny prefix typu ..., który sprawia, że funkcja może przyjąć nieograniczoną ilość argumentów. Tak jak na przykład fmt.Println. Zakładając, że mamy zaimplementowaną funkcję fmt.Print implementację fmt.Println wyobrażam sobie tak:

func Println(args ...interface{}) {
    last := len(args) - 1
    for i, v := range(args) {
        fmt.Print(v)
        if i == last {
            fmt.Print("\n")
            break;
        }
        fmt.Print(" ")
    }
}

Jak widać na powyższym przykładzie do identyfikatora poprzedającego ... zostaje przypisany wycinek, z wartościami typu po ....

Podobnie możemy przekazywać wycinki do funkcji. Wystarczy zmiennej zawierającej wycinek danego typu dopisać ....

Wartości zwracane z funkcji

Zazwyczaj miło jest jeśli z funkcji można zwrócić wartość. W Go jest bardzo miło, bo można tych wartości zwrócić wiele.

Nie mniej, zaczniemy od trywialnego przypadku, gdy chcemy zwrócić jedną wartość (typu int). Deklaracje o typie zwracanej wartości umieszczamy między listą parametrów a ciałem funkcji:

func sum(a, b int) int {
  return a + b
}

Gdy mamy zamiar zwrócić większą ilość zmiennych deklarujemy to za pomocą listy wartości, która ma taką samą składnię co lista parametrów. Np. tak:

func divMod(a, b int) (int, int) {
    return a/b, a%b
}

Możemy dodać identyfikatory przy typach spełniają one dwie role: po pierwsze deklaracji zmiennych, po drugie domyślnie zwracanych zmiennych. Funkcja divMod mogła by wyglądać tak:

func divMod(a, b int) (div int, mod int) {
     div, mod = a/b, a%b //jeśli nie rozumiesz tego zapisu przeczytaj o zmiennych
     return
}

Zauważ, że po return nie ma nic, mimo to funkcja zwróci odpowiednie wartości.

Warto pamiętać, że zmienne div i mod są już zadeklarowane (w zasięgu zmiennych owej funkcji), bo ich ponowna deklaracja w ciele funkcji wywoła błąd kompilacji.

Nie wspomniałem o tym, ale słowo kluczowe return przerywa wykonanie funkcji i przekazuje listę wartości następujących po nim jako wynik funkcji.

Przypisanie wyniku funkcji zwracającej wiele wartości do zmiennych, realizuje się następująco:

d, m := divMod(1,2)

Czyli wymieniamy nazwy zmiennych rozdzielając przecinkami i przypisujemy je do wywołania funkcji. Gdy z jakiegoś powodu jesteśmy zainteresowani tylko jedną ze zwracanych wartości możemy pozostałe przypisać do specjalnej pustej zmiennej _ :

Anonimowe funkcje

Przypomnę, że anonimowe funkcje tworzy się za pomocą literału funkcyjnego, który różni się tym od zwykłej deklaracji funkcji, że nie ma nazwy. Jak każda funkcja jest wartością (to jest można go przypisać do zmiennej lub wywołać w miejscu).

Przykład, przypisanie i wywołanie funkcji anonimowej:

Inny przykład, wywołanie funkcji anonimowej w miejscu:

Ciekawszym zastosowaniem jest filtrowanie wycinków. Przyjmijmy, ze funkcja filtrująca przyjmuje wycinek i funkcje, która dla każdego elementu wycinka zwróci true jeśli element ma zostać, a false gdy nie chcemy elementu w kolekcji:

func Filter(values []interface{}, satisfies func (interface{}) bool) (res []interface{}) {
    res = make([]interface{}, 0, len(values))
    for _, v := range(values) {
        if satisfies(v) {
           res = append(res, v)
        }
    }
    return
}

Czyli funkcja Filter przyjmuje wycinek wartości i funkcję, która zwraca prawdę lub fałsz dla zadanego elementu wycinka.

Teraz możemy tworzyć funkcje anonimowe, które dostosowują działanie funkcji Filter do potrzeb. Odfiltrujmy zatem wszystkie liczby parzyste z wycinka:

Wszystko fajnie, ale jak poradzić sobie z zadaniem wybierania co drugiego elementu z kolekcji? Do tego typu operacji (nie zmieniając funkcji Filter), możemy użyć domknięcia.

Domknięcie (clousure)

Domknięciem nazywamy funkcję z której zwracamy funkcję (anonimową). Dzięki właściwościom zasięgów funkcja zwracana dysponuje zakresem zmiennych funkcji tworzącej, który nie jest niszczony po wywołaniu.

Funkcja będzie zwracała true i false na przemiennie. Dzięki lekkiej modyfikacji powyższej funkcji możemy odfiltrować elementy o parzystych indeksach wycinka:

Dla utrwalenia wiedzy o domknięciach dodam jeszcze jeden przykład - jak deklarować funkcję, która pozwoli nam odfiltrować co n-ty wyraz.

Nawet zmienne z listy argumentów i listy wartości zwrotnych są dostępne w funkcji domykanej.

Będąc w temacie definiowania funkcji w funkcji należy pamiętać, że zagnieżdżać, można tylko literały funkcyjne, funkcje normalne - z nazwą - muszą być definiowane na poziomie pakietu.

Defer, czyli opóźnione wykonanie

To jak dla mnie nie spotykana nigdzie indziej konstrukcja, której zastosowanie trochę przypomina klauzule finaly w łapaniu wyjątków, ale do rzeczy:

Konstrukcja defer pozwala nam na wykonanie wyrażenia tuż przed przekazaniem wyniku funkcji (i sterowania) do bloku wywołującego ją. Jej składnia jest prosta: defer a po nim wyrażenie. Przykład:

Mimo, że funkcja f jawnie zwraca zero, to wywołanie funkcji inkrementującej wartość zwracaną tuż przed przekazaniem wyniku, skutkuje tym, że f() == 1. Bardziej pożytecznym zastosowaniem jest np. zamykanie otwieranego pliku czy zwalnianie mutexa.

W jednej funkcji można użyć defer wiele razy, wywołania będą wykonywane w kolejności od ostatniej do pierwszej.

Tą konstrucje najczęściej wykorzystujemy do łapania i obsługi błędów, w szczególności panic.

Metody

Metody to funkcje "dołączone" do nazwanego typu, po zdefiniowaniu metody możemy ją wykonywać na wartościach danego typu. Definicja metody różni się od definicji funkcji deklaracją typu odbiorcy, którego definiujemy między słowem func a nazwą. Na przykład:

Typowi myInt zdefiniowaliśmy metodę abs, która zwraca wartość bezwzględną.

Metody jako wartości

Od wersji Go 1.1 metody możemy przypisywać do zmiennych i traktować jak wartość funkcyjną. Będzie ona przywiązana do wartości z której ją wyciągamy. Brzmi nie zrozumiale? Pewnie tak, zatem zobaczmy jak to działa na przykładzie: