您现在的位置是:首页 >学无止境 >【七】Golang 函数网站首页学无止境

【七】Golang 函数

张胤尘 2026-03-22 12:01:04
简介【七】Golang 函数

函数

golang 中,函数是程序的基本组成单元。

函数签名

golang 中,函数签名(Function Signature)是函数的一个重要概念,它定义了函数的基本特征,用于区分不同的函数。

完整的函数签名格式如下:

func functionName(param1 type1, param2 type2, ...) returnType
  • functionName:函数的标识符,用于在代码中调用该函数。函数名必须是唯一的,以区分不同的函数。
  • param1 type1, param2 type2, ...:定义了函数接收的参数的类型和数量。参数列表包括参数的名称和类型。参数列表是函数签名的核心部分之一,用于确定函数的输入。
  • returnType:定义了函数返回的数据类型。返回值类型是函数签名的一部分,用于确定函数的输出。

函数签名的作用

  • 区分不同的函数:即使函数名相同,只要函数签名不同,它们就被视为不同的函数。例如:

    func add(a int, b int) int
    func add(a float64, b float64) float64
    

    这两个函数的签名不同,因此它们是不同的函数。

  • 类型检查:编译器通过函数签名来检查函数调用时参数的类型和数量是否匹配。

  • 接口实现:在 golang 的接口中,方法的签名用于确定是否实现了接口。如果一个类型的方法签名与接口中的方法签名完全一致,则该类型实现了该接口。

函数签名的比较

golang 中,函数签名的比较主要基于以下几点:

  • 函数名:函数名必须相同。
  • 参数列表:参数的数量和类型必须完全一致。
  • 返回值类型:返回值的数量和类型必须完全一致。

函数签名和方法签名

注意:方法签名与函数签名类似,但方法签名还包括接收者类型。方法签名的格式如下:

func (receiverType) methodName(param1 type1, param2 type2, ...) returnType

对于结构体中实现的函数一般称之为方法,所以对于函数签名和方法签名都是有区别的。后续在《Golang 结构体》章节着重讲解。

示例

例如,定义一个函数传入两个整型参数 ab,并返回它们的和(类型为 int):

package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func main() {

	a := 1
	b := 2

    // 调用 add 函数
	c := add(a, b)

	fmt.Println(c) // 3
}

参数与返回值

golang 中,函数的参数和返回值是函数定义和调用的核心部分,它们决定了函数如何接收输入以及如何返回输出。

函数参数

函数参数是函数定义中用于接收调用时传递的值的变量。参数在函数调用时被赋予具体的值,并在函数体内使用。

在函数签名的定义中,参数的格式为:

param1 type1, param2 type2, ...

由于 golang 是静态类型语言,因此在编译时必须确定参数的类型。例如:

func add(a int, b int) int {
    return a + b
}

其中,ab 是参数,类型都是 int

形参

golang 中形参是在函数定义时声明的参数。它们是函数的占位符,用于在函数内部接收外部传入的值。形参的类型和数量在函数定义时就已经确定。

形参包含以下的特点:

  • 形参在函数被调用时才会被赋予实际的值(即实参的值)。在函数未被调用时,形参只是函数签名的一部分,没有具体的值。
  • 形参的作用域仅限于函数内部。在函数外部是无法直接访问形参的。
func add(a int, b int) int {
    return a + b
}

在上面例子中,ab 就是形参。它们声明了函数 add 接收两个整型参数。

实参

golang 中实参是在调用函数时传递给函数的实际值。当函数被调用时,实参的值会被传递到函数内部,赋值给对应的形参。

实参包含以下的特点:

  • 实参的类型必须与形参的类型一致,或者可以隐式转换为形参的类型。否则,编译器会报错。
  • 实参可以是变量、常量、表达式等,只要它们的类型符合形参的要求。
package main

import "fmt"

func add(a int, b int) int {
    return a + b
}

func main() {
    x := 5
    y := 10
    result := add(x, y)
    fmt.Println(result)	// 15
}

在这个例子中,xy 是实参。它们的值在调用 add 函数时被传递给形参 ab

参数的传递方式

golang 中,参数的传递方式是值传递。当函数被调用时,实参的值会被复制到形参中,同时在函数体内对形参的修改不会影响原始的实参值。

package main

import "fmt"

func modifyValue(x int) {
	x = 100
}

func main() {
	a := 10

	// 函数调用传递采用的是值传递
	modifyValue(a)
	fmt.Println(a) // 输出 10,原始值没有被修改
}

当然,如果需要在函数内部改变传递到函数中的实参值,那么可以采用传递指针的方式,如下所示:

package main

import "fmt"

func modifyValue(x *int) {
	*x = 100
	fmt.Println(x)  // x指针指向的内存地址:0xc00009a040
	fmt.Println(&x) // x指针的内存地址:0xc000108048
}

func main() {
	a := 10
	ptr := &a

	fmt.Println(ptr)  // ptr指针指向的内存地址:0xc00009a040
	fmt.Println(&ptr) // ptr指针的内存地址:0xc000108038

	// 函数调用传递采用的是值传递
	modifyValue(ptr)
	fmt.Println(a) // 输出 100,原始值被修改
}

需要注意的是,x 指针本身传递到函数 modifyValue 中时采用的也是值传递,也就是说函数 x 的指针和 ptr 指针并不是同一个指针,但是这两个指针都指向了变量 a 内存空间,所以在函数 modifyValue 中可以使用 x 来修改变量 a 的值。

可变长参数

golang 中,可变长参数(也称为可变参数)允许函数接收不定数量的参数。这种特性使得函数更加灵活,能够处理多种情况,而无需为每种情况定义不同的函数版本。

可变长参数的定义使用 ... 符号,后面跟着参数的类型。可变长参数必须是函数的最后一个参数。其语法格式如下:

func functionName(param1 type1, param2 type2, ..., variadicParam ...type) returnType {
    // 函数体
}

以下是一个接收可变长参数的函数示例:

package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, num := range nums {
		total += num
	}
	return total
}

func main() {
	a := 1
	b := 2
	c := 10

	fmt.Println(sum())        // 0
	fmt.Println(sum(a, b))    // 3
	fmt.Println(sum(a, b, c)) // 13
}

在这个例子中,nums 是一个可变长参数,可以接收任意数量的 int 类型参数,包括零个参数。

可变长参数的内部表示

golang 中在函数内部,可变长参数被表示为一个切片(slice。因此,可以使用切片的所有操作,例如遍历、索引访问等。

示例参考上面的代码

传递切片作为可变长参数

如果已经有一个切片,可以直接将其作为可变长参数传递。这需要在切片变量后面加上 ... 符号。

package main

import "fmt"

func sum(nums ...int) int {
	total := 0
	for _, num := range nums {
		total += num
	}
	return total
}

func main() {
	numbers := []int{1, 2, 3, 4, 5}
	result := sum(numbers...)   // 使用 ... 将切片展开为可变长参数
	fmt.Println("Sum:", result) // Sum: 15
}
注意事项
  • 必须是最后一个参数:可变长参数必须是函数定义中的最后一个参数。
  • 类型必须一致:可变长参数的所有值必须是相同的类型。
  • 不能与其他可变长参数共存:一个函数只能有一个可变长参数。
参数的省略

golang 提供语法糖,当函数形参的类型相同时,可以省略参数的类型。

package main

import "fmt"

func add(a, b int) int {	// 函数中的 a, b都是 int 类型
	return a + b
}

func main() {
	c := add(1, 2)
	fmt.Println(c)	// 3
}

函数返回值

多返回值

golang 支持函数返回多个值。多返回值的语法如下:

func functionName(param1 type1, param2 type2, ...) (returnType1, returnType2, ...) {
    // 函数体
}

例如:

package main

func divide(x, y int) (int, int) {
    return x / y, x % y
}

在调用函数时,可以同时接收多个返回值:

quotient, remainder := divide(10, 3) // quotient == 3, remainder == 1

在这个例子中,divide 函数返回两个值:商和余数。

命名返回值

golang 允许为返回值命名,这可以提高代码的可读性并简化返回值的赋值。命名返回值的语法如下:

func functionName(param1 type1, param2 type2, ...) (result1 returnType1, result2 returnType2) {
    // 函数体
    return
}

例如:

func rectangleArea(width, height int) (area int) {
    area = width * height
    return
}

在这个例子中,area 是命名返回值,可以直接在函数体内赋值,最后使用 return 语句返回。

匿名函数

golang 中,匿名函数是一种没有函数名的函数,它可以在需要时直接定义和使用。
匿名函数的定义与普通函数类似,但不需要指定函数名。其语法格式如下:

func(参数列表)(返回参数列表) {
    // 函数体
}

例如,定义一个简单的匿名函数,接收一个 int 类型的参数,并打印输出:

package main

import "fmt"

func main() {
	func(data int) {
		fmt.Println("hello", data) // hello 100
	}(100)
}

匿名函数存在以下特点:

  • 没有名字:匿名函数没有函数名,因此不能通过名字调用。
  • 可以定义在函数内部:匿名函数可以嵌套定义在其他函数内部。
  • 可以作为变量使用:匿名函数可以赋值给变量,然后通过变量调用。
  • 可以作为参数或返回值:匿名函数可以作为参数传递给其他函数,也可以作为函数的返回值。

匿名函数赋值给变量

匿名函数可以赋值给变量,然后通过变量调用:

package main

import "fmt"

func main() {
	add := func(a, b int) int {
		return a + b
	}
	result := add(3, 5)
	fmt.Println("3 + 5 =", result) // 3 + 5 = 8
}

这种方式使得匿名函数可以像普通函数一样被多次调用。

匿名函数作为参数传递

golang 中支持匿名函数可以作为参数传递给其他函数,这在回调函数、事件处理等场景中非常有用:

package main

import "fmt"

func execute(f func(int, int) int, a, b int) {
	result := f(a, b)
	fmt.Println("结果:", result) // 结果: 8
}

func main() {

	add := func(a, b int) int {
		return a + b
	}

	execute(add, 3, 5)
}

上面代码中 execute 函数接受一个匿名函数作为参数 f func(int, int) int,并在函数中调用它。

匿名函数作为返回值

golang 中也同样支持匿名函数可以作为函数的返回值,如下实例所示:

package main

import "fmt"

func getMultiplier(factor int) func(int) int {
	return func(x int) int {
		return x * factor
	}
}

func main() {

	double := getMultiplier(2)
	fmt.Println("2 * 3 =", double(3)) // 2 * 3 = 6
	fmt.Println("2 * 5 =", double(5)) // 2 * 5 = 10
}

示例代码中 getMultiplier 函数返回一个匿名函数 func(int) int,该匿名函数可以多次调用。

闭包

golang 中,闭包是一种特殊的匿名函数,它可以捕获并保存其定义时所在作用域中的变量。闭包使得这些变量在匿名函数的生命周期内保持有效,即使外部作用域已经结束执行。

闭包的工作原理基于以下几点:

  • 捕获变量:闭包可以捕获其定义时所在作用域中的变量。
  • 变量生命周期:捕获的变量在闭包的生命周期内保持有效。
  • 延迟计算:闭包可以在定义后延迟执行,捕获的变量值在闭包执行时才被使用。
package main

import "fmt"

func main() {
    x := 10
    increment := func() {
        x++
        fmt.Println("x =", x)
    }

    increment() // x = 11
    increment() // x = 12
}

在上面的代码中 x 是一个局部变量,定义在 main 函数中。increment 是一个匿名函数,捕获了变量 x。每次调用 increment 时,x 的值都会被修改并打印。

闭包作为返回值

闭包可以作为函数的返回值,代码如下所示:

package main

import "fmt"

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

func main() {
	add5 := makeAdder(5)
	fmt.Println(add5(10)) // 15
	fmt.Println(add5(20)) // 25
}

首先 makeAdder 函数返回一个匿名函数,返回的这个匿名函数接收一个 int 类型的参数,并返回 makeAdder 函数的参数和匿名函数参数的和,下面分析三次调用的流程:

  • add5 := makeAdder(5) 执行后 add5 是一个函数,inc 的值为5
  • 第一次执行调用 add5(10),传入 x 的值是10,inc 的值为5,结果为15
  • 第二次执行调用 add5(20),传入 x 的值是20,inc 的值为5,结果为25

闭包与并发

闭包在并发编程中也需要特别注意,因为捕获的变量可能会被多个 go routine 同时访问和修改。为了避免竞态条件,需要使用同步机制:

package main

import (
	"fmt"
	"sync"
)

func counter() func() int {
	var count int
	return func() int {
		count++
		return count
	}
}

func main() {
	// 1
	// 2
	// 3
	// 4
	// 5
	// 6
	// 7
	// 8
	// 9
	// 10

	next := counter()
	var wg sync.WaitGroup	// 使用 sync.WaitGroup 确保所有 go routine 完成后主线程才退出
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println(next())	// 每个线程打印 count 的值,每次 count 都会自增
		}()
	}
	wg.Wait()
}

后续在《Golang 并发编程》章节会着重介绍协程、sync.WaitGroup 相关知识点

注意事项

  • 变量捕获:闭包捕获的是变量的引用,而不是值的副本。因此,对捕获的变量的修改会影响所有引用该变量的闭包。
  • 内存泄漏:如果闭包捕获了较大的变量或资源,可能会导致内存泄漏。需要确保闭包的生命周期合理管理。
  • 并发安全:如果闭包捕获的变量会被多个 go routine 访问和修改,需要使用同步机制(如 sync.Mutex)来避免竞态条件。

递归函数

递归函数是一种非常强大的编程思想,它允许函数在其内部调用自身。递归函数通常用于解决可以分解为相同子问题的问题,例如数学问题、数据结构遍历和分治算法等。

递归函数是指在函数体内直接或间接调用自身的函数。递归函数通常包含两个基本要素:

  • 基准条件:这是递归的终止条件,当达到这个条件时,递归将停止。
  • 递归模式:这是递归函数如何将大问题分解为小问题的方法。

golang 中,编写递归函数的基本步骤如下:

  • 定义一个函数,函数内部调用自身:递归函数通过调用自身来解决更小规模的问题。
  • 在函数体内,添加递归终止条件:为了避免无限循环,递归函数必须有明确的终止条件。
  • 根据需要,传递参数给递归调用的函数:递归函数可以通过参数控制递归调用的行为。

计算阶乘

以下是一个计算阶乘的递归函数示例:

package main

import "fmt"

func factorial(n int) int {
	if n == 1 {
		return 1
	}
	return n * factorial(n-1)	// 递归调用 factorial 函数
}

func main() {
	i := 5
	res := factorial(i)

	fmt.Println("阶乘执行结果: ", res) // 阶乘执行结果:  120
}

斐波那契数列

计算斐波那契数列的递归函数示例:

斐波那契数列(Fibonacci sequence)是一个非常著名的数列,它由以下递归关系定义:

F(n)=F(n−1)+F(n−2)

其中,F(0)=0 和 F(1)=1 是数列的初始条件。根据这个定义,斐波那契数列的前几项为:

0,1,1,2,3,5,8,13,21,34,…

package main

import "fmt"

func fibonacci(n int) int {
	if n == 0 {
		return 0
	}

	if n == 1 {
		return 1
	}

	return fibonacci(n-1) + fibonacci(n-2)
}

func main() {

	i := 7
	res := fibonacci(i)

	fmt.Println("Fibonacci 执行结果: ", res) // Fibonacci 执行结果:  13
}

特殊函数

main 函数

golang 中,main 函数是一个非常重要的特殊函数,它是每个可执行程序的入口点。当程序启动时,它将执行 main 函数中的代码。如果程序中没有 main 函数,或者 main 函数不在主包(package main)中,那么程序将无法编译成可执行文件。

  • 入口点:main 函数是程序执行的起点
  • 无参数:main 函数不接受任何参数
  • 无返回值:main 函数不返回任何值
  • 单一性:每个可执行程序只能有一个 main 函数
  • 包限制:main 函数必须位于主包中,即包名必须是 main
执行流程
  • 初始化全局变量:在进入 main 函数之前,程序会初始化所有全局变量。
  • 调用 init 函数:如果存在,程序会按照在代码中出现的顺序调用所有包的 init 函数。
  • 执行 main 函数:完成初始化后,程序开始执行 main 函数中的代码。
  • 退出程序:当 main 函数执行完毕,程序结束运行。

init 函数

golang 语言中,init 函数是一种特殊的函数,它用于初始化包(package)级别的变量。每个包可以包含多个 init 函数,它们在程序启动时,也就是进入 main 函数之前,按照它们在代码中出现的顺序被调用。init 函数主要用于执行全局变量的初始化、配置日志、设置环境变量等需要在程序开始运行之前完成的操作。

  • 自动调用:init 函数在程序启动时自动调用,无需显式调用
  • 无参数和返回值:init 函数没有参数,也不返回任何值
  • 无显式返回:即使 init 函数包含可执行语句,它也不需要显式返回,编译器会自动处理
  • 初始化顺序:如果包之间存在依赖关系,那么依赖的包的 init 函数会先于依赖它的包执行
  • 递归调用:如果包 A 依赖于包 B,而包 B 又依赖于包 C,那么 init 函数的调用顺序将是 C -> B -> A
package main

func init() {
	println("init function call ...")
}

func main() {
	println("main function call ...")
}

// init function call ...
// main function call ...
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。