您现在的位置是:首页 >学无止境 >【七】Golang 函数网站首页学无止境
【七】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结构体》章节着重讲解。
示例
例如,定义一个函数传入两个整型参数 a 和 b,并返回它们的和(类型为 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
}
其中,a 和 b 是参数,类型都是 int。
形参
golang 中形参是在函数定义时声明的参数。它们是函数的占位符,用于在函数内部接收外部传入的值。形参的类型和数量在函数定义时就已经确定。
形参包含以下的特点:
- 形参在函数被调用时才会被赋予实际的值(即实参的值)。在函数未被调用时,形参只是函数签名的一部分,没有具体的值。
- 形参的作用域仅限于函数内部。在函数外部是无法直接访问形参的。
func add(a int, b int) int {
return a + b
}
在上面例子中,a 和 b 就是形参。它们声明了函数 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
}
在这个例子中,x 和 y 是实参。它们的值在调用 add 函数时被传递给形参 a 和 b。
参数的传递方式
在 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 ...





QT多线程的5种用法,通过使用线程解决UI主界面的耗时操作代码,防止界面卡死。...
U8W/U8W-Mini使用与常见问题解决
stm32使用HAL库配置串口中断收发数据(保姆级教程)
分享几个国内免费的ChatGPT镜像网址(亲测有效)
Allegro16.6差分等长设置及走线总结