ZetCode

Go 函数

最后修改于 2025 年 5 月 11 日

本文全面概述了 Go 语言中函数的使用。函数在高效组织代码、实现可重用逻辑以及确保程序更清晰、更易于维护方面发挥着至关重要的作用。

函数定义

Go 中的函数是代码单元,它将零个或多个输入参数映射到零个或多个输出参数。函数使开发人员能够封装逻辑、提高代码可读性并高效地构建应用程序。

在 Go 中使用函数的主要优点包括

在 Go 中,函数是“一等公民”,这意味着它们可以被赋值给变量、作为参数传递以及从其他函数返回。这种灵活性支持了像高阶函数和函数组合这样的强大编程技术。

Go 中的函数使用 func 关键字定义。return 语句用于从函数返回。函数体包含函数调用时执行的语句。函数体包含在花括号 {} 中,确保逻辑清晰分隔。

要调用函数,必须指定其名称,后跟括号 ()。函数可以接受零个或多个参数,从而根据输入值实现动态行为。通过在函数中组织逻辑,Go 程序变得更具可伸缩性、可维护性,并且更易于调试。

简单示例

以下示例在 Go 中创建了一个简单的函数。

main.go
package main

import "fmt"

func main() {


    x := 4
    y := 5

    z := add(x, y)

    fmt.Printf("Output: %d\n", z)
}

func add(a int, b int) int {

    return a + b
}

在代码示例中,我们定义了一个将两个值相加的函数。

z := add(x, y)

我们调用 add 函数;它接受两个参数。计算出的值被传递给 z 变量。

func add(a int, b int) int {

    return a + b
}

我们定义了 add 函数。函数的参数用逗号分隔;每个参数名称后跟其数据类型。在参数之后,我们指定返回值的类型。函数调用时执行的语句放在花括号之间。使用 return 关键字将加法运算的结果返回给调用者。

$ go run main.go 
Output: 9

省略类型

当函数的参数具有相同的类型时,可以省略其中一些参数的类型;也就是说,只指定一次。

main.go
package main

import "fmt"

func add(x int, y int) int {

    return x + y
}

func sub(x, y int) int {

    return x - y
}

func main() {


    fmt.Println(add(5, 4))
    fmt.Println(sub(5, 4))
}

在代码示例中,我们有两个函数:addsub。在 sub 函数的情况下,为 x 变量省略了类型。

命名返回变量的函数

我们可以在函数参数后的圆括号中指定命名返回变量。

main.go
package main

import "fmt"

func inc(x, y, z int) (a, b, c int) {

    a = x + 1
    b = y + 1
    c = z + 1

    return
}

func main() {


    x, y, z := inc(10, 100, 1000)

    fmt.Println(x, y, z)
}

在代码示例中,我们有一个递增其三个参数的函数。

func inc(x, y, z int) (a, b, c int) {

我们有三个命名返回值为:abc

a = x + 1
b = y + 1
c = z + 1

return

我们计算返回变量的值。之后,我们必须指定 return 关键字。

$ go run main.go 
11 101 1001

多个返回值

Go 函数允许返回多个值。

main.go
package main

import (
    "fmt"
    "math/rand"
)

func threerandom() (int, int, int) {

    x := rand.Intn(10)
    y := rand.Intn(10)
    z := rand.Intn(10)

    return x, y, z
}

func main() {


    r1, r2, r3 := threerandom()

    fmt.Println(r1, r2, r3)
}

在代码示例中,我们有一个 threerandom 函数,它返回三个随机值。

func threerandom() (int, int, int) {

我们指定该函数返回三个整数值。

x := rand.Intn(10)
y := rand.Intn(10)
z := rand.Intn(10)

我们计算三个随机值。

return x, y, z

这些值是从函数返回的。它们用逗号分隔。

$ go run main.go 
0 8 0

匿名函数

我们可以创建匿名函数。匿名函数没有名称。

main.go
package main

import "fmt"

func main() {


    sum := func(a, b, c int) int {
        return a + b + c
    }(3, 5, 7)

    fmt.Println("5+3+7 =", sum)
}

我们创建了一个将三个值相加的匿名函数。我们在其定义后立即向该函数传递三个参数。

$ go run main.go 
5+3+7 = 15

可变参数函数

可变参数函数可以接受可变数量的参数。例如,当我们想计算值的总和时,我们可能需要向函数传递四个、五个、六个或更多值。

我们使用 ...(省略号)运算符来定义可变参数函数。

main.go
package main

import "fmt"

func main() {


    s1 := sum(1, 2, 3)
    s2 := sum(1, 2, 3, 4)
    s3 := sum(1, 2, 3, 4, 5)

    fmt.Println(s1, s2, s3)
}

func sum(nums ...int) int {

    res := 0

    for _, n := range nums {
        res += n
    }

    return res
}

在代码示例中,我们有一个 sum 函数,它接受可变数量的参数。

func sum(nums ...int) int {

    res := 0

    for _, n := range nums {
        res += n
    }

    return res
}

nums 变量是一个切片,其中包含传递给 sum 函数的所有值。我们循环遍历切片并计算参数的总和。

$ go run main.go 
6 10 15

递归函数

递归,在数学和计算机科学中,是一种定义方法的途径,其中所定义的方法在其自身的定义中被应用。换句话说,递归方法调用自身来完成其任务。递归是解决许多编程任务的常用方法。

一个典型的例子是计算阶乘。

main.go
package main

import "fmt"

func fact(n int) int {

    if n == 0 || n == 1 {
        return 1
    }

    return n * fact(n-1)
}

func main() {


    fmt.Println(fact(7))
    fmt.Println(fact(10))
    fmt.Println(fact(15))
}

在此代码示例中,我们计算了三个数字的阶乘。

return n * fact(n-1)

fact 函数的体内部,我们用修改过的参数调用 fact 函数。该函数调用自身。

$ go run main.go 
5040
3628800
1307674368000

这些是计算出的阶乘。

延迟函数调用

defer 语句将函数执行推迟到外部函数返回为止。被推迟调用的参数会立即评估,但函数调用要到外部函数返回后才执行。

main.go
package main

import "fmt"

func main() {


    fmt.Println("begin main")

    defer sayHello()

    fmt.Println("end main")
}

func sayHello() {

    fmt.Println("hello")
}

在代码示例中,sayHello 函数在 main 函数完成后被调用。

$ go run main.go 
begin main
end main
hello

按值传递参数

在 Go 中,函数参数仅按值传递。

注意: 在 Go 中,一切都按值传递。当我们传递一个类型指针时,会创建一个该指针的独立副本。这与 C 不同,在 C 中,对于指针,会将同一个指针传递给函数。

在下面的示例中,一个整数和一个 User 结构被作为参数传递给函数。

main.go
package main

import "fmt"

type User struct {
    name       string
    occupation string
}

func main() {


    x := 10
    fmt.Printf("inside main %d\n", x)

    inc(x)

    fmt.Printf("inside main %d\n", x)

    fmt.Println("---------------------")

    u := User{"John Doe", "gardener"}
    fmt.Printf("inside main %v\n", u)

    change(u)
    fmt.Printf("inside main %v\n", u)
}

func inc(x int) {

    x++
    fmt.Printf("inside inc %d\n", x)
}

func change(u User) {

    u.occupation = "driver"
    fmt.Printf("inside change %v\n", u)
}

在代码示例中,原始的 xUser 结构值未被修改。

func inc(x int) {

    x++
    fmt.Printf("inside inc %d\n", x)
}

创建了整数值的副本。在函数内部,我们递增该副本的值。因此,原始变量保持不变。

$ go run main.go 
inside main 10
inside inc 11
inside main 10
---------------------
inside main {John Doe gardener}
inside change {John Doe driver}
inside main {John Doe gardener}

在下一个示例中,我们传递了整数变量和结构的指针。

main.go
package main

import "fmt"

type User struct {
    name       string
    occupation string
}

func main() {


    x := 10
    fmt.Printf("inside main %d\n", x)

    inc(&x)

    fmt.Printf("inside main %d\n", x)

    fmt.Println("---------------------")

    u := User{"John Doe", "gardener"}
    fmt.Printf("inside main %v\n", u)

    change(&u)
    fmt.Printf("inside main %v\n", u)
}

func inc(x *int) {

    (*x)++
    fmt.Printf("inside inc %d\n", *x)
}

func change(u *User) {

    u.occupation = "driver"
    fmt.Printf("inside change %v\n", *u)
}

现在原始值已被修改。但从技术上讲,参数仍然是按值传递的。Go 创建了指针的新副本。(这与 C 不同。)

inc(&x)

使用 & 字符,我们传递了 x 变量的指针。

func inc(x *int) {

    (*x)++
    fmt.Printf("inside inc %d\n", *x)
}

创建了指向 x 变量的指针的副本。更改 x 的值也会修改原始变量。

$ go run main.go 
inside main 10
inside inc 11
inside main 11
---------------------
inside main {John Doe gardener}
inside change {John Doe driver}
inside main {John Doe driver}

原始值已被修改。

数组是值类型,切片和映射是引用类型。因此,对于切片和映射,会创建引用的副本。

main.go
package main

import "fmt"

func main() {


    vals := []int{1, 2, 3, 4, 5}

    fmt.Printf("%v\n", vals)

    square(vals)

    fmt.Printf("%v\n", vals)
}

func square(vals []int) {

    for i, val := range vals {

        vals[i] = val * val
    }
}

在代码示例中,我们将一个切片传递给 square 函数。原始切片的元素被修改。

$ go run main.go 
[1 2 3 4 5]
[1 4 9 16 25]

元素被平方。

数组是值类型。

main.go
package main

import "fmt"

func main() {


    vals := [5]int{1, 2, 3, 4, 5}

    fmt.Printf("%v\n", vals)

    square(vals)

    fmt.Printf("%v\n", vals)
}

func square(vals [5]int) {

    for i, val := range vals {

        vals[i] = val * val
    }
}

示例将一个数组传递给 square 函数。

$ go run main.go 
[1 2 3 4 5]
[1 2 3 4 5]

元素未被修改。

映射是引用类型。

main.go
package main

import "fmt"

func main() {


    items := map[string]int{"coins": 1, "pens": 2, "chairs": 4}
    fmt.Printf("%v\n", items)

    update(items)

    fmt.Printf("%v\n", items)
}

func update(items map[string]int) {

    items["coins"] = 6
}

示例将一个映射传递给 update 函数。

$ go run main.go 
map[chairs:4 coins:1 pens:2]
map[chairs:4 coins:6 pens:2]

正如我们所见,原始映射已被更新。

函数作为参数

Go 函数可以作为参数传递给其他函数。这样的函数称为高阶函数。

main.go
package main

import "fmt"

func inc(x int) int {
    x++
    return x
}

func dec(x int) int {
    x--
    return x
}

func apply(x int, f func(int) int) int {

    r := f(x)
    return r
}

func main() {

    r1 := apply(3, inc)
    r2 := apply(2, dec)
    fmt.Println(r1)
    fmt.Println(r2)
}

在代码示例中,apply 函数接受 incdec 函数作为参数。

func apply(x int, f func(int) int) int {

我们指定第二个参数是函数类型。

r1 := apply(3, inc)
r2 := apply(2, dec)

我们将 incdec 函数作为参数传递给 apply 函数。

$ go run main.go 
4
1

自定义函数类型

Go 允许使用 type 关键字创建可重用的函数签名。

main.go
package main

import "fmt"

type output func(string) string

func hello(name string) string {

    return fmt.Sprintf("hello %s", name)
}

func main() {

    var f output

    f = hello
    fmt.Println(f("Peter"))
}

使用 type 关键字,我们创建了一个接受一个字符串参数并返回一个字符串的函数类型。

filter 函数

我们有一个实际的示例,用于过滤数据。

main.go
package main

import "fmt"

type User struct {
    name       string
    occupation string
    married    bool
}

func main() {


    u1 := User{"John Doe", "gardener", false}
    u2 := User{"Richard Roe", "driver", true}
    u3 := User{"Bob Martin", "teacher", true}
    u4 := User{"Lucy Smith", "accountant", false}
    u5 := User{"James Brown", "teacher", true}

    users := []User{u1, u2, u3, u4, u5}

    married := filter(users, func(u User) bool {
        if u.married == true {
            return true
        }
        return false
    })

    teachers := filter(users, func(u User) bool {

        if u.occupation == "teacher" {
            return true
        }
        return false
    })

    fmt.Println("Married:")
    fmt.Printf("%v\n", married)

    fmt.Println("Teachers:")
    fmt.Printf("%v\n", teachers)

}

func filter(s []User, f func(User) bool) []User {
    var res []User

    for _, v := range s {

        if f(v) == true {
            res = append(res, v)
        }
    }
    return res
}

我们有一个 User 结构的切片。我们过滤该切片,形成已婚用户和教师用户的新切片。

married := filter(users, func(u User) bool {
    if u.married == true {
        return true
    }
    return false
})

我们调用 filter 函数。它接受一个匿名函数作为参数。对于已婚用户,该函数返回 true。返回布尔值的函数也称为谓词

func filter(s []User, f func(User) bool) []User {
    var res []User

    for _, v := range s {

        if f(v) == true {
            res = append(res, v)
        }
    }
    return res
}

filter 函数为满足给定条件的所有用户形成一个新切片。

$ go run main.go 
Married:
[{Richard Roe driver true} {Bob Martin teacher true} {James Brown teacher true}]
Teachers:
[{Bob Martin teacher true} {James Brown teacher true}]

函数作为结构方法

在 Go 中,您可以通过在 func 关键字和方法名之间指定接收器来定义结构类型的函数。

main.go
package main

import "fmt"

type Rectangle struct {
    width, height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

// Method with pointer receiver
func (r *Rectangle) Scale(factor float64) {
    r.width *= factor
    r.height *= factor
}

func main() {

    rect := Rectangle{width: 10, height: 5}
    
    fmt.Println("Initial area:", rect.Area())
    
    rect.Scale(2)
    fmt.Println("After scaling:")
    fmt.Println("Width:", rect.width, "Height:", rect.height)
    fmt.Println("New area:", rect.Area())
}

此示例演示了结构方法的指针和值接收器。

func (r Rectangle) Area() float64 {

Area 是一个具有值接收器的函数 - 它操作 Rectangle 的副本。

func (r *Rectangle) Scale(factor float64) {

Scale 是一个具有指针接收器的函数 - 它可以修改原始 Rectangle。

$ go run main.go 
Initial area: 50
After scaling:
Width: 20 Height: 10
New area: 200

带 Worker 接口的函数

此示例演示了如何使用接口来创建与不同 worker 实现配合使用的灵活函数。

main.go
package main

import (
    "fmt"
    "time"
)

type Worker interface {
    Work() string
    Rest()
}

type Programmer struct {
    Name string
}

func (p Programmer) Work() string {
    return fmt.Sprintf("%s is writing Go code", p.Name)
}

func (p Programmer) Rest() {
    fmt.Printf("%s is taking a coffee break\n", p.Name)
}

type Chef struct {
    Name string
}

func (c Chef) Work() string {
    return fmt.Sprintf("%s is preparing a gourmet meal", c.Name)
}

func (c Chef) Rest() {
    fmt.Printf("%s is tasting the food\n", c.Name)
}

func WorkDay(w Worker, hours int) {
    fmt.Println("Starting work day")
    for i := 0; i < hours; i++ {
        fmt.Println(w.Work())
        time.Sleep(500 * time.Millisecond) // Simulate work
    }
    w.Rest()
    fmt.Println("Work day complete")
}

func main() {
    dev := Programmer{"Alice"}
    cook := Chef{"Bob"}

    WorkDay(dev, 3)
    WorkDay(cook, 2)
}

此示例展示了不同的类型如何实现相同的接口并由同一个函数处理。

type Worker interface {
    Work() string
    Rest()
}

Worker 接口定义了任何想要被视为 Worker 的类型都必须实现的两个方法。

func WorkDay(w Worker, hours int) {
    fmt.Println("Starting work day")
    for i := 0; i < hours; i++ {
        fmt.Println(w.Work())
        time.Sleep(500 * time.Millisecond)
    }
    w.Rest()
    fmt.Println("Work day complete")
}

WorkDay 函数接受任何满足 Worker 接口的类型,并协调工作日。

$ go run main.go 
Starting work day
Alice is writing Go code
Alice is writing Go code
Alice is writing Go code
Alice is taking a coffee break
Work day complete
Starting work day
Bob is preparing a gourmet meal
Bob is preparing a gourmet meal
Bob is tasting the food
Work day complete

带类型开关的函数

此示例演示了如何在函数内部使用类型开关来区别处理不同类型。

main.go
package main

import (
    "fmt"
    "reflect"
)

func ProcessValue(v interface{}) string {
    switch val := v.(type) {
    case int:
        return fmt.Sprintf("Integer: %d (double is %d)", val, val*2)
    case float64:
        return fmt.Sprintf("Float: %.2f (square is %.2f)", val, val*val)
    case string:
        return fmt.Sprintf("String: '%s' (length %d)", val, len(val))
    case bool:
        return fmt.Sprintf("Boolean: %v (negated is %v)", val, !val)
    default:
        return fmt.Sprintf("Unknown type: %v", reflect.TypeOf(v))
    }
}

func main() {

    fmt.Println(ProcessValue(42))
    fmt.Println(ProcessValue(3.14159))
    fmt.Println(ProcessValue("Hello"))
    fmt.Println(ProcessValue(true))
    fmt.Println(ProcessValue([]int{1, 2, 3}))
}

此函数使用类型开关为不同的输入类型提供不同的处理。

func ProcessValue(v interface{}) string {
    switch val := v.(type) {
    case int:
        return fmt.Sprintf("Integer: %d (double is %d)", val, val*2)
    ...
    }
}

类型开关检查接口值的具体类型并执行匹配的 case。

fmt.Println(ProcessValue(42))
fmt.Println(ProcessValue(3.14159))
fmt.Println(ProcessValue("Hello"))

同一个函数以适当的行为处理多种不同的类型。

$ go run main.go 
Integer: 42 (double is 84)
Float: 3.14 (square is 9.87)
String: 'Hello' (length 5)
Boolean: true (negated is false)
Unknown type: []int

立即调用函数表达式(IIFE)

在 Go 中,您可以定义并立即执行匿名函数。这种模式对于初始化或创建作用域很有用。

main.go
package main

import (
    "fmt"
    "time"
)

func main() {

    // IIFE that initializes a value
    startTime := func() time.Time {
        fmt.Println("Initializing start time...")
        return time.Now()
    }()
    
    // IIFE with parameters
    func(msg string) {
        fmt.Println("Message:", msg)
    }("Hello from IIFE")
    
    fmt.Println("Program started at:", startTime.Format("15:04:05"))
    
    // IIFE creating a scope
    func() {
        x := 10
        y := 20
        fmt.Println("Inside scope:", x+y)
    }()
    
    // x and y not accessible here
}

此示例展示了 Go 中立即调用函数表达式的多种用法。

startTime := func() time.Time {
    fmt.Println("Initializing start time...")
    return time.Now()
}()

一个 IIFE,它使用当前时间初始化一个变量。

func(msg string) {
    fmt.Println("Message:", msg)
}("Hello from IIFE")

一个接受参数并立即执行的 IIFE。

$ go run main.go 
Initializing start time...
Message: Hello from IIFE
Program started at: 14:30:45
Inside scope: 30

带泛型类型参数的函数

Go 1.18 引入了泛型,允许函数在保持类型安全的同时处理多种类型。

main.go
package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// Generic function to find the maximum of two values
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Generic function to reverse any slice
func ReverseSlice[T any](s []T) []T {
    result := make([]T, len(s))
    for i, v := range s {
        result[len(s)-1-i] = v
    }
    return result
}

func main() {

    fmt.Println("Max int:", Max(3, 7))
    fmt.Println("Max float:", Max(3.14, 2.71))
    fmt.Println("Max string:", Max("apple", "banana"))

    ints := []int{1, 2, 3, 4, 5}
    strs := []string{"a", "b", "c", "d"}

    fmt.Println("Reversed ints:", ReverseSlice(ints))
    fmt.Println("Reversed strs:", ReverseSlice(strs))

    type Point struct{ X, Y float64 }
    points := []Point{{1, 2}, {3, 4}, {5, 6}}
    fmt.Println("Reversed points:", ReverseSlice(points))
}

此示例演示了处理多种类型的泛型函数。

func Max[T constraints.Ordered](a, b T) T {

Max 函数使用类型参数 T,该参数约束为有序类型,允许像 > 这样的比较。

func ReverseSlice[T any](s []T) []T {

ReverseSlice 函数适用于任何类型(any 约束)。

fmt.Println("Max int:", Max(3, 7))
fmt.Println("Max float:", Max(3.14, 2.71))

同一个泛型函数适用于不同的具体类型。

$ go run main.go 
Max int: 7
Max float: 3.14
Max string: banana
Reversed ints: [5 4 3 2 1]
Reversed strs: [d c b a]
Reversed points: [{5 6} {3 4} {1 2}]

函数中的错误处理

Go 函数通常返回一个错误值作为其第二个返回参数,以指示何时出现问题。

main.go
package main

import (
    "errors"
    "fmt"
    "math"
)

func sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, errors.New("cannot take square root of negative number")
    }
    return math.Sqrt(x), nil
}

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {


    if result, err := sqrt(9); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Square root:", result)
    }
    
    if result, err := divide(10, 0); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Division result:", result)
    }
    
    if result, err := sqrt(-4); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Square root:", result)
    }
}

此示例演示了从函数返回错误的惯用 Go 模式。

func sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, errors.New("cannot take square root of negative number")
    }
    return math.Sqrt(x), nil
}

sqrt 函数返回一个有效结果和一个 nil 错误,或者返回零和一个错误消息。

if result, err := sqrt(9); err != nil {
    fmt.Println("Error:", err)
} else {
    fmt.Println("Square root:", result)
}

调用代码首先检查错误值,然后再使用结果。

$ go run main.go 
Square root: 3
Error: division by zero
Error: cannot take square root of negative number

闭包

Go 中的闭包是从封闭函数返回的匿名函数。闭包保留对在其主体外部定义的变量的引用。

main.go
package main

import "fmt"

func intSeq() func() int {

    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {


    nextInt := intSeq()

    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())

    nextInt2 := intSeq()
    fmt.Println(nextInt2())
}

我们有 intSeq 函数,它生成一个整数序列。它返回一个递增 i 变量的闭包。

func intSeq() func() int {

intSeq 是一个返回返回整数的函数的函数。

func intSeq() func() int {

    i := 0
    return func() int {
        i++
        return i
    }
}

在函数中定义的变量具有局部函数作用域。但是,在这种情况下,即使在 `intSeq` 函数返回后,闭包仍绑定到 `i` 变量。

nextInt := intSeq()

我们调用 intSeq 函数。它返回一个将递增计数器的函数。闭包存储在 nextInt 变量中。

fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())

我们多次调用闭包。

$ go run main.go 
1
2
3
4
1

高阶函数

高阶函数是指接受函数作为参数或返回函数的函数。

main.go
package main

import "fmt"

func main() {


    x := 3
    y := 4

    add, sub := getAddSub()

    r1, r2 := apply(x, y, add, sub)

    fmt.Printf("%d + %d = %d\n", x, y, r1)
    fmt.Printf("%d - %d = %d\n", x, y, r2)
}

func apply(x, y int, add func(int, int) int, sub func(int, int) int) (int, int) {

    r1 := add(x, y)
    r2 := sub(x, y)

    return r1, r2
}

func getAddSub() (func(int, int) int, func(int, int) int) {

    add := func(x, y int) int {
        return x + y
    }

    sub := func(x, y int) int {
        return x - y
    }

    return add, sub
}

在代码示例中,我们对函数进行了一些复杂的操作。apply 函数接受两个函数作为参数。getAddSub 函数返回两个函数。

$ go run main.go 
3 + 4 = 7
3 - 4 = -1

带 Context 参数的函数

Go 的 context 包通常用于跨 API 边界的请求范围值、取消信号和截止时间。它有助于管理操作的生命周期,尤其是在并发编程中。

main.go
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchData simulates a long-running operation while respecting context cancellation.
func fetchData(ctx context.Context, query string) (string, error) {
    select {
    case <-time.After(1 * time.Second): // Simulating query processing delay
        return fmt.Sprintf("Results for '%s'", query), nil
    case <-ctx.Done(): // Handling context cancellation
        return "", fmt.Errorf("query canceled for '%s'", query)
    }
}

func main() {

    // Create a context with a timeout of 1 second
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel() // Ensure context cleanup to avoid resource leaks

    // Channels to receive results or errors from the goroutine
    resultChan := make(chan string)
    errChan := make(chan error)

    // Launch a goroutine to execute fetchData
    go func() {
        res, err := fetchData(ctx, "golang generics")
        if err != nil {
            errChan <- err // Send error to error channel
            return
        }
        resultChan <- res // Send result to result channel
    }()

    // Wait for the operation to complete or the timeout to expire
    select {
    case res := <-resultChan:
        fmt.Println("Success:", res)
    case err := <-errChan:
        fmt.Println("Error:", err)
    case <-ctx.Done():
        fmt.Println("Main context expired")
    }
}

此示例演示了 context 如何在并发操作中实现适当的超时和取消处理。fetchData 函数模拟了一个长时间运行的任务,并等待两种可能的结果之一:模拟两秒延迟(代表查询的处理时间),或来自提供上下文的取消信号。如果上下文在两秒内过期,函数将立即停止执行并返回错误。

func fetchData(ctx context.Context, query string) (string, error) {
    select {
    case <-time.After(1 * time.Second):
        return fmt.Sprintf("Results for '%s'", query), nil
    case <-ctx.Done():
        return "", fmt.Errorf("query canceled for '%s'", query)
    }
}

fetchData 函数尊重上下文取消,这使得它对于处理不应无限期运行的操作特别有用。在生产环境中,这种方法通常应用于 API 请求和数据库查询,以避免长时间运行的任务阻塞系统资源。

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

在这里,context.WithTimeout 创建了一个截止时间为一秒的新上下文。当达到超时时,上下文会自动取消与之绑定的任何操作。cancel 函数确保与上下文关联的资源得到妥善清理。使用超时可以防止过度等待,并确保应用程序更具响应性。

select {
case res := <-resultChan:
    fmt.Println("Success:", res)
case err := <-errChan:
    fmt.Println("Error:", err)
case <-ctx.Done():
    fmt.Println("Main context expired")
}

此选择机制侦听三种可能的结果:如果 resultChan 接收到数据,则打印成功结果;如果 errChan 接收到错误,则打印失败消息;如果 ctx.Done 触发,则表示请求已超时。使用 select 块可确保程序根据最快的可用结果动态响应。

$ go run main.go 
Error: query canceled for 'golang generics'

由于超时设置为一秒,而查询模拟需要两秒,因此在操作完成之前就被取消了。结果是,返回了一个错误消息而不是查询结果。调整超时持续时间可以根据需求微调应用程序的响应能力。

使用 context 对于有效管理并发操作至关重要。它确保任务不会无限期地继续消耗资源,并使不同组件之间的执行流程得以控制。这种方法在网络应用程序中尤其有用,在网络应用程序中,如果不进行适当管理,延迟可能会影响系统性能。

来源

The Go Programming Language Specification

在本文中,我们介绍了 Go 语言中的函数。

作者

我的名字是 Jan Bodnar,我是一名充满激情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。到目前为止,我已撰写了 1400 多篇文章和 8 本电子书。我在教学编程方面有十多年的经验。

列出所有 Go 教程