Skip to content

Go-Funktionen

In Go sind Funktionen First-Class-Citizen. Funktionen sind die grundlegendste Komponente von Go und auch der Kern von Go.

Deklaration

Das Format der Funktionsdeklaration lautet wie folgt:

go
func Funktionsname([Parameterliste]) [Rückgabewert] {
  Funktionskörper
}

Es gibt zwei Möglichkeiten, eine Funktion zu deklarieren. Eine ist die direkte Deklaration mit dem Schlüsselwort func, die andere ist die Deklaration mit dem Schlüsselwort var, wie unten gezeigt:

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

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

Die Funktionssignatur besteht aus Funktionsname, Parameterliste und Rückgabewert. Nachfolgend ein vollständiges Beispiel: Der Funktionsname ist Sum, es gibt zwei int-Typ-Parameter a, b, der Rückgabetyp ist int.

go
func Sum(a int, b int) int {
   return a + b
}

Ein weiterer sehr wichtiger Punkt ist, dass Funktionen in Go keine Überladung unterstützen. Der folgende Code kann nicht kompiliert werden:

go
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}
}

Die Philosophie von Go lautet: Wenn die Signatur unterschiedlich ist, dann sind es zwei völlig verschiedene Funktionen, daher sollte nicht derselbe Name verwendet werden. Funktionsüberladung kann den Code verwirrend und schwer verständlich machen. Ob diese Philosophie korrekt ist, ist Ansichtssache. Zumindest können Sie in Go allein durch den Funktionsnamen wissen, was er tut, ohne suchen zu müssen, welcher Überladung er angehört.

Parameter

In Go können Parameternamen weggelassen werden. Dies wird im Allgemeinen nur bei Schnittstellen- oder Funktionstypdeklarationen verwendet. Aus Gründen der Lesbarkeit wird jedoch empfohlen, Parameternamen möglichst hinzuzufügen:

go
type ExWriter func(io.Writer) error

type Writer interface {
  ExWrite([]byte) (int, error)
}

Für Parameter desselben Typs muss der Typ nur einmal deklariert werden, jedoch müssen sie benachbart sein:

go
func Log(format string, a1, a2 any) {
  ...
}

Variadische Parameter können 0 oder mehr Werte empfangen und müssen am Ende der Parameterliste deklariert werden. Das typischste Beispiel ist die fmt.Printf-Funktion.

go
func Printf(format string, a ...any) (n int, err error) {
  return Fprintf(os.Stdout, format, a...)
}

Erwähnenswert ist, dass Funktionsparameter in Go per Wertübertragung übergeben werden, d. h. beim Übergeben von Parametern wird der Wert des tatsächlichen Parameters kopiert. Wenn Sie denken, dass beim Übergeben von Slices oder Maps viel Speicher kopiert wird, kann ich Ihnen versichern, dass dies nicht der Fall ist, da diese beiden Datenstrukturen im Wesentlichen Zeiger sind.

Rückgabewert

Nachfolgend ein einfaches Beispiel für einen Funktionsrückgabewert. Die Sum-Funktion gibt einen int-Typ-Wert zurück.

go
func Sum(a, b int) int {
   return a + b
}

Wenn eine Funktion keinen Rückgabewert hat, ist kein void erforderlich. Ein Rückgabewert wird einfach nicht angegeben.

go
func ErrPrintf(format string, a ...any) {
  _, _ = fmt.Fprintf(os.Stderr, format, a...)
}

Go erlaubt Funktionen mehrere Rückgabewerte. In diesem Fall müssen die Rückgabewerte in Klammern gesetzt werden.

go
func Div(a, b float64) (float64, error) {
  if a == 0 {
    return math.NaN(), errors.New("0 kann nicht als Divisor verwendet werden")
  }
  return a / b, nil
}

Go unterstützt auch benannte Rückgabewerte, die nicht mit Parameternamen übereinstimmen dürfen. Bei Verwendung benannter Rückgabewerte muss das return-Schlüsselwort nicht angeben, welche Werte zurückgegeben werden.

go
func Sum(a, b int) (ans int) {
  ans = a + b
  return
}

Wie bei Parametern können bei mehreren benannten Rückgabewerten desselben Typs wiederholte Typdeklarationen weggelassen werden:

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
  return
}

Unabhängig davon, wie benannte Rückgabewerte deklariert werden, haben die Werte nach dem return-Schlüsselwort immer die höchste Priorität.

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
    // c, d werden nicht zurückgegeben
  return a + b, a * b
}

Anonyme Funktionen

Eine anonyme Funktion ist eine Funktion ohne Signatur. Zum Beispiel die Funktion func(a, b int) int im folgenden Beispiel. Sie hat keinen Namen, daher müssen wir unmittelbar nach dem Funktionskörper Klammern hinzufügen, um sie aufzurufen.

go
func main() {
   func(a, b int) int {
      return a + b
   }(1, 2)
}

Beim Aufruf einer Funktion, wenn ihr Parameter ein Funktionstyp ist, ist der Name nicht mehr wichtig. Es kann direkt eine anonyme Funktion übergeben werden, wie unten gezeigt:

go
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
  })
}

Dies ist ein Beispiel für benutzerdefinierte Sortierregeln. slices.SortFunc akzeptiert zwei Parameter: einen Slice und eine Vergleichsfunktion. Wenn Wiederverwendung nicht berücksichtigt wird, können wir direkt eine anonyme Funktion übergeben.

Closure

Eine Closure (auch als Lambda-Ausdruck in einigen Sprachen bekannt) zusammen mit anonymen Funktionen: Closure = Funktion + Umgebungsreferenz. Sehen Sie sich das folgende Beispiel an:

go
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
  }
}

Ausgabe:

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=512

Die Exp-Funktion gibt eine Funktion zurück, die hier als grow-Funktion bezeichnet wird. Jedes Mal, wenn sie aufgerufen wird, wächst die Variable e exponentiell. Die grow-Funktion referenziert zwei Variablen der Exp-Funktion: e und n. Sie entstehen im Gültigkeitsbereich der Exp-Funktion. Normalerweise würde der Speicher dieser Variablen nach dem Verlassen des Stacks freigegeben, sobald der Aufruf der Exp-Funktion beendet ist. Da die grow-Funktion sie jedoch referenziert, können sie nicht freigegeben werden, sondern entkommen auf den Heap. Selbst wenn der Lebenszyklus der Exp-Funktion beendet ist, ist der Lebenszyklus der Variablen e und n nicht beendet. Innerhalb der grow-Funktion können diese beiden Variablen direkt geändert werden. Die grow-Funktion ist eine Closure-Funktion.

Mit Closures kann eine Funktion zur Berechnung der Fibonacci-Folge sehr einfach implementiert werden. Der Code lautet wie folgt:

go
func main() {
    // 10 Fibonacci-Zahlen
  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
  }
}

Ausgabe:

0
1
1
2
3
5
8
13
21
34

Verzögerter Aufruf

Das Schlüsselwort defer ermöglicht es, eine Funktion verzögert aufzurufen. Vor der Rückgabe der Funktion werden diese durch defer beschriebenen Funktionen nacheinander ausgeführt. Siehe folgendes Beispiel:

go
func main() {
  Do()
}

func Do() {
  defer func() {
    fmt.Println("1")
  }()
  fmt.Println("2")
}

Ausgabe:

2
1

Da defer vor der Rückgabe der Funktion ausgeführt wird, können Sie den Rückgabewert der Funktion auch in defer ändern:

go
func main() {
  fmt.Println(sum(3, 5))
}

func sum(a, b int) (s int) {
  defer func() {
    s -= 10
  }()
  s = a + b
  return
}

Wenn mehrere durch defer beschriebene Funktionen vorhanden sind, werden sie in der Reihenfolge eines Stacks (Last-In-First-Out) ausgeführt.

go
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
1

Verzögerte Aufrufe werden typischerweise zum Freigeben von Dateiresourcen, Schließen von Netzwerkverbindungen usw. verwendet. Eine weitere Verwendungsmöglichkeit ist das Abfangen von panic, dies wird jedoch erst im Abschnitt über Fehlerbehandlung behandelt.

Schleife

Obwohl es nicht ausdrücklich verboten ist, wird im Allgemeinen empfohlen, defer nicht in for-Schleifen zu verwenden, wie unten gezeigt:

go
func main() {
  n := 5
  for i := range n {
    defer fmt.Println(i)
  }
}

Ausgabe:

4
3
2
1
0

Das Ergebnis dieses Codes ist korrekt, aber der Prozess ist möglicherweise nicht richtig. In Go muss für jedes erstellte defer ein Speicherbereich im aktuellen Goroutine angefordert werden. Angenommen, im obigen Beispiel handelt es sich nicht um eine einfache for n-Schleife, sondern um einen komplexeren Datenverarbeitungsprozess. Wenn die Anzahl der externen Anfragen plötzlich stark ansteigt, werden in kurzer Zeit viele defer erstellt. Bei sehr großen oder unbestimmten Schleifenanzahlen kann dies zu einem plötzlichen Anstieg des Speicherverbrauchs führen. Dies bezeichnen wir im Allgemeinen als Speicherleck.

Parameter-Vorberechnung

Für verzögerte Aufrufe gibt es einige kontraintuitive Details. Zum Beispiel dieses:

go
func main() {
  defer fmt.Println(Fn1())
  fmt.Println("3")
}

func Fn1() int {
  fmt.Println("2")
  return 1
}

Diese Falle ist sehr versteckt. Der Autor hatte früher aufgrund dieser Falle Schwierigkeiten, die Ursache zu finden. Sie können raten, was die Ausgabe ist. Die Antwort lautet:

2
3
1

Viele Menschen denken vielleicht, die Ausgabe wäre wie folgt:

3
2
1

Nach der Absicht des Benutzers sollte fmt.Println(Fn1()) ausgeführt werden, nachdem der Funktionskörper ausgeführt wurde. fmt.Println wird tatsächlich zuletzt ausgeführt, aber Fn1() wird unerwartet früher ausgeführt. Das folgende Beispiel macht die Situation noch deutlicher.

go
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
}

Die Ausgabe ist definitiv 3 und nicht 7. Wenn anstelle des verzögerten Aufrufs eine Closure verwendet wird, ist das Ergebnis anders:

go
func main() {
  var a, b int
  a = 1
  b = 2
  f := func() {
    fmt.Println(sum(a, b))
  }
  a = 3
  b = 4
  f()
}

Die Ausgabe der Closure ist 7. Was passiert, wenn verzögerter Aufruf und Closure kombiniert werden?

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func() {
    fmt.Println(sum(a, b))
  }()
  a = 3
  b = 4
}

Diesmal ist es normal, die Ausgabe ist 7. Ändern wir es erneut, ohne Closure:

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func(num int) {
    fmt.Println(num)
  }(sum(a, b))
  a = 3
  b = 4
}

Die Ausgabe wird wieder zu 3. Durch den Vergleich der obigen Beispiele lässt sich feststellen, dass dieser Code:

defer fmt.Println(sum(a,b))

tatsächlich äquivalent ist zu:

defer fmt.Println(3)

Go wartet nicht bis zum Schluss, um die sum-Funktion aufzurufen. Die sum-Funktion wird bereits vor der Ausführung des verzögerten Aufrufs aufgerufen und als Parameter an fmt.Println übergeben. Zusammenfassend lässt sich sagen, dass für die direkt von defer betroffene Funktion ihre Parameter vorberechnet werden. Dies führt zu dem seltsamen Phänomen im ersten Beispiel. Besonders bei verzögerten Aufrufen, bei denen der Rückgabewert einer Funktion als Parameter übergeben wird, ist Vorsicht geboten.

Golang by www.golangdev.cn edit