Funzioni Go
In Go le funzioni sono cittadini di prima classe, le funzioni sono la componente più basilare di Go e sono anche il nucleo di Go.
Dichiarazione
Il formato di dichiarazione di una funzione è il seguente
func nomeFunzione([listaParametri]) [valoreDiRitorno] {
corpoFunzione
}Ci sono due modi per dichiarare una funzione, uno è dichiarare direttamente con la parola chiave func, l'altro è dichiarare con la parola chiave var, come mostrato di seguito
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}La firma di una funzione è composta dal nome della funzione, dalla lista dei parametri e dal valore di ritorno. Di seguito è riportato un esempio completo, il nome della funzione è Sum, ha due parametri di tipo int a e b, il tipo di ritorno è int.
func Sum(a int, b int) int {
return a + b
}C'è anche un punto molto importante, ovvero che le funzioni in Go non supportano l'overloading. Il codice seguente non può superare la compilazione
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}
}La filosofia di Go è: se le firme sono diverse, allora sono due funzioni completamente diverse, quindi non dovrebbero avere lo stesso nome. L'overloading delle funzioni renderebbe il codice confuso e difficile da capire. Se questa filosofia sia corretta o meno dipende dai punti di vista, almeno in Go puoi sapere cosa fa una funzione solo dal suo nome, senza dover cercare quale overload sia.
Parametri
I nomi dei parametri in Go possono non avere un nome, generalmente questo viene utilizzato solo nelle dichiarazioni di interfacce o tipi di funzione, ma per la leggibilità si consiglia comunque di aggiungere nomi ai parametri
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}Per parametri dello stesso tipo, è necessario dichiarare il tipo una sola volta, ma la condizione è che devono essere adiacenti
func Log(format string, a1, a2 any) {
...
}I parametri di lunghezza variabile possono ricevere 0 o più valori, devono essere dichiarati alla fine della lista dei parametri. L'esempio più tipico è la funzione fmt.Printf.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}Vale la pena notare che il passaggio dei parametri di funzione in Go è per valore, ovvero quando si passano i parametri viene copiato il valore del parametro effettivo. Se pensi che passare slice o map comporti la copia di grandi quantità di memoria, posso dirti che non c'è bisogno di preoccuparsi, poiché queste due strutture dati sono essenzialmente puntatori.
Valore di ritorno
Di seguito è riportato un semplice esempio di valore di ritorno di funzione, la funzione Sum restituisce un valore di tipo int.
func Sum(a, b int) int {
return a + b
}Quando una funzione non ha valore di ritorno, non è necessario void, basta non includere il valore di ritorno.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go consente alle funzioni di avere più valori di ritorno, in questo caso è necessario racchiudere i valori di ritorno tra parentesi.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 non può essere il divisore")
}
return a / b, nil
}Go supporta anche valori di ritorno denominati, che non possono essere duplicati con i nomi dei parametri. Quando si utilizzano valori di ritorno denominati, la parola chiave return non deve specificare quali valori restituire.
func Sum(a, b int) (ans int) {
ans = a + b
return
}Come per i parametri, quando ci sono più valori di ritorno denominati dello stesso tipo, è possibile omettere le dichiarazioni di tipo duplicate
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}Indipendentemente da come sono dichiarati i valori di ritorno denominati, i valori dopo la parola chiave return hanno sempre la priorità più alta.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d non verranno restituiti
return a + b, a * b
}Funzioni anonime
Una funzione anonima è una funzione senza firma, ad esempio la funzione func(a, b int) int di seguito, non ha un nome, quindi possiamo chiamarla solo seguendo immediatamente il suo corpo con parentesi.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}Quando si chiama una funzione, quando il suo parametro è un tipo di funzione, il nome non è più importante, è possibile passare direttamente una funzione anonima, come mostrato di seguito
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
})
}Questo è un esempio di regola di ordinamento personalizzata, slices.SortFunc accetta due parametri, uno è una slice e l'altro è una funzione di confronto. Se non si considera il riutilizzo, possiamo passare direttamente una funzione anonima.
Closure
Una closure, concetto noto in alcuni linguaggi anche come espressione Lambda, utilizzata con funzioni anonime, closure = funzione + riferimento all'ambiente. Guarda l'esempio seguente:
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=512Il valore di ritorno della funzione Exp è una funzione, qui chiamata funzione grow. Ogni volta che viene chiamata, la variabile e cresce esponenzialmente. La funzione grow fa riferimento a due variabili della funzione Exp: e e n, che sono nate nell'ambito della funzione Exp. In condizioni normali, con la fine della chiamata della funzione Exp, la memoria di queste variabili verrebbe recuperata con l'uscita dallo stack. Tuttavia, poiché la funzione grow le fa riferimento, non possono essere recuperate, ma sfuggono all'heap. Anche se il ciclo di vita della funzione Exp è terminato, il ciclo di vita delle variabili e e n non è terminato. All'interno della funzione grow è ancora possibile modificare queste due variabili, la funzione grow è una funzione closure.
Utilizzando le closure, è possibile implementare molto semplicemente una funzione per calcolare la sequenza di Fibonacci, il codice è il seguente
func main() {
// 10 numeri di Fibonacci
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
}
}L'output è
0
1
1
2
3
5
8
13
21
34Chiamata differita
La parola chiave defer consente di ritardare la chiamata di una funzione per un periodo di tempo. Prima che la funzione restituisca, queste funzioni descritte da defer verranno eseguite una per una. Guarda l'esempio seguente
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Output
2
1Poiché defer viene eseguito prima del ritorno della funzione, puoi anche modificare il valore di ritorno della funzione in defer
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}Quando ci sono più funzioni descritte da defer, verranno eseguite nell'ordine LIFO (Last In First Out) come uno stack.
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
1Le chiamate differite sono solitamente utilizzate per rilasciare risorse file, chiudere connessioni di rete e altre operazioni. Un altro utilizzo è catturare panic, ma questo sarà trattato nella sezione sulla gestione degli errori.
Ciclo
Sebbene non sia esplicitamente vietato, si consiglia generalmente di non utilizzare defer nei cicli for, come mostrato di seguito
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}L'output è il seguente
4
3
2
1
0Il risultato di questo codice è corretto, ma il processo potrebbe non essere corretto. In Go, ogni volta che si crea un defer, è necessario richiedere uno spazio di memoria nel goroutine corrente. Supponiamo che nell'esempio precedente non si tratti di un semplice ciclo for n, ma di un flusso di elaborazione dati più complesso. Quando il numero di richieste esterne aumenta improvvisamente, verranno creati molti defer in breve tempo. Quando il numero di cicli è elevato o incerto, ciò potrebbe causare un aumento improvviso dell'occupazione della memoria, che generalmente definiamo come perdita di memoria.
Pre-calcolo dei parametri
Per le chiamate differite ci sono alcuni dettagli controintuitivi, ad esempio l'esempio seguente
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}Questa trappola è molto subdola, l'autore ha impiegato mezza giornata per capire quale fosse la causa. Puoi provare a indovinare qual è l'output, la risposta è la seguente
2
3
1Molte persone potrebbero pensare che l'output sia il seguente
3
2
1Secondo l'intenzione dell'utente, fmt.Println(Fn1()) dovrebbe essere eseguito dopo che il corpo della funzione è stato eseguito. fmt.Println viene effettivamente eseguito per ultimo, ma Fn1() è inaspettato. L'esempio seguente rende la situazione ancora più evidente.
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
}Il suo output è sicuramente 3 e non 7. Se si utilizza una closure invece di una chiamata differita, il risultato è diverso
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}L'output della closure è 7. E se combiniamo chiamata differita e closure?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}Questa volta è normale, l'output è 7. Modifichiamo di nuovo, senza closure
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}L'output torna a essere 3. Confrontando gli esempi precedenti, si può scoprire che questo codice
defer fmt.Println(sum(a,b))è in realtà equivalente a
defer fmt.Println(3)Go non aspetta fino alla fine per chiamare la funzione sum, la funzione sum viene chiamata prima che la chiamata differita venga eseguita e viene passata come parametro a fmt.Println. In sintesi, per la funzione su cui agisce direttamente defer, i suoi parametri verranno pre-calcolati, il che porta allo strano fenomeno nel primo esempio. Per questa situazione, specialmente quando il valore di ritorno di una funzione viene passato come parametro in una chiamata differita, è necessario prestare particolare attenzione.
