Fonctions en Go
En Go, les fonctions sont des citoyens de première classe. Les fonctions sont la composante la plus fondamentale de Go et sont au cœur de Go.
Déclaration
Le format de déclaration d'une fonction est le suivant :
func nomFonction([listeParamètres]) [valeurRetour] {
corpsFonction
}Il y a deux façons de déclarer une fonction : l'une est de déclarer directement avec le mot-clé func, l'autre est de déclarer avec le mot-clé var, comme suit :
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}La signature d'une fonction est composée du nom de la fonction, de la liste des paramètres et de la valeur de retour. Voici un exemple complet : le nom de la fonction est Sum, il y a deux paramètres de type int : a et b, et le type de retour est int.
func Sum(a int, b int) int {
return a + b
}Un point très important est que les fonctions en Go ne prennent pas en charge la surcharge. Le code suivant ne passera pas la compilation :
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 philosophie de Go est la suivante : si les signatures sont différentes, ce sont deux fonctions complètement différentes, donc elles ne devraient pas avoir le même nom. La surcharge de fonctions rendrait le code confus et difficile à comprendre. Que cette philosophie soit correcte ou non dépend de chacun, mais au moins en Go, vous pouvez savoir ce que fait une fonction simplement par son nom, sans avoir à chercher quelle surcharge elle est.
Paramètres
Les noms des paramètres en Go peuvent être omis, généralement utilisé uniquement lors de la déclaration d'interfaces ou de types de fonctions. Cependant, pour la lisibilité, il est généralement recommandé d'ajouter des noms aux paramètres.
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}Pour des paramètres de même type, le type peut être déclaré une seule fois, à condition qu'ils soient adjacents.
func Log(format string, a1, a2 any) {
...
}Les paramètres de longueur variable peuvent recevoir 0 ou plusieurs valeurs et doivent être déclarés à la fin de la liste des paramètres. L'exemple le plus typique est la fonction fmt.Printf.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}Il convient de mentionner que le passage de paramètres de fonctions en Go se fait par valeur, c'est-à-dire que la valeur du paramètre réel est copiée lors du passage. Si vous pensez que la transmission de tranches ou de maps copiera une grande quantité de mémoire, je peux vous dire que vous n'avez pas à vous en soucier, car ces deux structures de données sont essentiellement des pointeurs.
Valeurs de retour
Voici un exemple simple de valeur de retour de fonction, la fonction Sum retourne une valeur de type int.
func Sum(a, b int) int {
return a + b
}Lorsqu'une fonction n'a pas de valeur de retour, void n'est pas nécessaire, il suffit de ne pas inclure de valeur de retour.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go permet aux fonctions d'avoir plusieurs valeurs de retour. Dans ce cas, les valeurs de retour doivent être entourées de parenthèses.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 ne peut pas être le diviseur")
}
return a / b, nil
}Go prend également en charge les valeurs de retour nommées, qui ne doivent pas être dupliquées avec les noms de paramètres. Lors de l'utilisation de valeurs de retour nommées, le mot-clé return n'a pas besoin de spécifier quelles valeurs retourner.
func Sum(a, b int) (ans int) {
ans = a + b
return
}De même que pour les paramètres, lorsqu'il y a plusieurs valeurs de retour nommées de même type, les déclarations de type répétées peuvent être omises.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}Quelle que soit la manière dont les valeurs de retour nommées sont déclarées, les valeurs après le mot-clé return ont toujours la priorité la plus élevée.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d ne seront pas retournés
return a + b, a * b
}Fonctions anonymes
Une fonction anonyme est une fonction sans signature. Par exemple, la fonction func(a, b int) int ci-dessous n'a pas de nom, nous devons donc suivre immédiatement le corps de la fonction avec des parenthèses pour l'appeler.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}Lors de l'appel d'une fonction, lorsque son paramètre est un type de fonction, le nom n'est plus important, et vous pouvez directement passer une fonction anonyme, comme suit :
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
})
}Ceci est un exemple de règle de tri personnalisée. slices.SortFunc accepte deux paramètres : une tranche et une fonction de comparaison. Si vous ne tenez pas compte de la réutilisation, vous pouvez directement passer une fonction anonyme.
Fermetures
Le concept de fermeture (Closure), appelé expression Lambda dans certains langages, utilisé avec des fonctions anonymes. Une fermeture = fonction + référence à l'environnement. Voici un exemple :
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
}
}Sortie :
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=512La valeur de retour de la fonction Exp est une fonction, ici appelée fonction grow. Chaque fois qu'elle est appelée, la variable e augmente de manière exponentielle. La fonction grow référence deux variables de la fonction Exp : e et n. Elles sont nées dans le champ d'application de la fonction Exp. Normalement, la mémoire de ces variables serait récupérée lorsque la fonction Exp se termine. Cependant, comme la fonction grow les référence, elles ne peuvent pas être récupérées, mais s'échappent plutôt vers le tas. Même si le cycle de vie de la fonction Exp est terminé, le cycle de vie des variables e et n n'est pas terminé. Dans la fonction grow, ces deux variables peuvent encore être directement modifiées. La fonction grow est une fonction de fermeture.
En utilisant des fermetures, vous pouvez implémenter très simplement une fonction pour calculer la suite de Fibonacci. Voici le code :
func main() {
// 10 nombres de 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
}
}La sortie est :
0
1
1
2
3
5
8
13
21
34Appel différé
Le mot-clé defer permet de retarder l'appel d'une fonction pendant un certain temps. Avant le retour de la fonction, ces fonctions décrites par defer seront finalement exécutées une par une. Voici un exemple :
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Sortie :
2
1Comme defer est exécuté avant le retour de la fonction, vous pouvez également modifier la valeur de retour de la fonction dans defer.
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}Lorsqu'il y a plusieurs fonctions décrites par defer, elles seront exécutées dans l'ordre dernier entré, premier sorti (LIFO), comme une pile.
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
1Les appels différés sont généralement utilisés pour libérer des ressources de fichiers, fermer des connexions réseau, etc. Une autre utilisation est de capturer panic, mais cela sera abordé dans la section sur la gestion des erreurs.
Boucles
Bien qu'il n'y ait pas d'interdiction explicite, il est généralement recommandé de ne pas utiliser defer dans les boucles for, comme suit :
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}Sortie :
4
3
2
1
0Le résultat de ce code est correct, mais le processus peut être incorrect. En Go, chaque création d'un defer nécessite l'allocation d'un espace mémoire dans la goroutine actuelle. Supposons que dans l'exemple ci-dessus, au lieu d'une simple boucle for n, il s'agisse d'un processus de traitement de données plus complexe. Lorsque le nombre de requêtes externes augmente soudainement, un grand nombre de defer seront créés en peu de temps. Lorsque le nombre de boucles est très élevé ou incertain, cela peut entraîner une augmentation soudaine de l'utilisation de la mémoire, ce que nous appelons généralement une fuite de mémoire.
Pré-calcul des paramètres
Il y a des détails contre-intuitifs concernant les appels différés. Par exemple, dans l'exemple suivant :
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}Ce piège est très subtil. L'auteur a eu du mal à en identifier la raison pendant une demi-journée auparavant. Vous pouvez deviner quelle est la sortie. La réponse est la suivante :
2
3
1Beaucoup de gens pourraient penser que la sortie est la suivante :
3
2
1Selon l'intention de l'utilisateur, fmt.Println(Fn1()) devrait être exécuté après l'exécution du corps de la fonction. fmt.Println est effectivement exécuté en dernier, mais Fn1() est inattendu. La situation de l'exemple suivant est encore plus évidente.
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
}Sa sortie sera certainement 3 et non 7. Si vous utilisez une fermeture au lieu d'un appel différé, le résultat sera différent.
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}La sortie de la fermeture est 7. Et si nous combinons appel différé et fermeture ?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}Cette fois, c'est normal, la sortie est 7. Modifions à nouveau, sans fermeture :
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}La sortie redevient 3. En comparant les exemples ci-dessus, on peut constater que ce code :
defer fmt.Println(sum(a,b))est en fait équivalent à :
defer fmt.Println(3)Go n'attend pas la fin pour appeler la fonction sum. La fonction sum est appelée avant que l'appel différé ne soit exécuté et est passée comme paramètre à fmt.Println. En résumé, pour la fonction sur laquelle defer agit directement, ses paramètres seront pré-calculés, ce qui conduit au phénomène étrange du premier exemple. Cette situation nécessite une attention particulière, en particulier lorsque la valeur de retour d'une fonction est passée comme paramètre dans un appel différé.
