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 funkcje, rzadko występują w normalnym programowaniu, wyjątkiem 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 wyliczy same typy. Jedyne sensowne wykorzystanie takiej cechy języka widzę w przypadku potrzeby spełnienia konkretnego interface (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ć ....
package main import ( "fmt" ) func Println(args ...string) { last := len(args) - 1 for i, v := range(args) { fmt.Print(v) if i == last { fmt.Print("\n") break; } fmt.Print(" ") } } func main() { s := []string{"a","b","c"} Println(s...) }
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, ale zacznijmy 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 sygnalizujemy to listą wartości co do składni identyczną jak lista parametrów.
func divMod(a, b int) (int, int) { return a/b, a%b }
Napisałem że lista parametrów ma identyczną składnię jak lista wartości zwracanych. Identyfikatory przy typach spełniają dwie role: po pierwsze deklaracji zmiennych, po drugie domyślnych wartości zwracanych. 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 }
zobacz także: Definicja zmiennej
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 listujemy identyfikatory zmiennych rozdzielając je przecinkami i przypisujemy 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 _ :
_, modulo := divMod(3,4)
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 i jak każda funkcja jest wartością (to jest można go przypisać do zmiennej lub wywołać w miejscu). Jako przykładowe zastosowanie użyję filtrowania wycinków. Przyjmijmy, ze funkcja filtrująca wygląda następująco:
func Filter(value []interface{}, satisfies func (interface{}) bool) (res []interface{}) { res = make([]interface{}, 0, len(v)) for i, v := range(value) { if satisfies(v) { res = append(res, v) } } return }
Teraz możemy tworzyć funkcje anonimowe, które dostosowują działanie funkcji
Filter do potrzeb. Odfiltrujmy wszystkie liczby parzyste z wycinka:
intS := []interface{}{0,2,3,4,5,12,22,35,65} evens := Filter(intS, func(v interface{}) bool { i, ok := v.(int) return ok && (i % 2 == 0) }); fmt.Println(evens)
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
Domknięciem nazywamy funkcję w której tworzymy funkcję anonimową, dzięki właściwościom zasięgów funkcja tworzona dysponuje zakresem zmiennych funkcji tworzącej i nie jest niszczona po wywołaniu.
func clousure() func(interface{}) bool { i := 0 return func (interface{}) bool { i++ //Zmienna z zakresu domknięcięcia return (i - 1) % 2 == 0 } }
Dzięki powyższej funkcji możemy odfiltrować elementy o parzystych indeksach wycinka:
intS := []interface{}{0,2,3,4,5,12,22,35,65} everyOther := Filter(intS, clousure()); fmt.Println(everyOther)
Dla utrwalenia wiedzy o domknięciach dodam jeszcze jeden przykład - jak deklarować funkcję, która pozwoli nam odfiltrować co n-ty wyraz.
func nClousure(n int) func(interface{}) bool { i := 0 return func (interface{}) bool { i++ return (i - 1) % n == 0 //n jest widoczna! } }
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:
func f() (result int) { defer func() { result++ }() return 0 }
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.
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:
type myInt int64 func (i myInt) abs() myInt { if i < 0 { return -i } return i } func main() { fmt.Println(myInt(-11).abs()) //wyprowadzi 11 na ekran }
Typowi myInt zdefiniowaliśmy metodę abs, która zwraca wartość bezwzględną.
Wszystko o języku programowania Go