您现在的位置是:首页 >技术交流 >04-编织灵魂旋律:Golang 函数的魔力绽放网站首页技术交流
04-编织灵魂旋律:Golang 函数的魔力绽放
?个人主页:个人主页
?系列专栏:Golang基础
?Go(又称Golang)是由Google开发的开源编程语言。它结合了静态类型的安全性和动态语言的灵活性,拥有高效的并发编程能力和简洁的语法。Go被设计用于构建可扩展、高性能的软件系统,具有优秀的内存管理和快速的编译速度,适用于Web开发、系统编程和云计算等领域。
函数的定义
函数 是Go里面的核心设计,它通过关键字 func 来声明,它的格式如下:
func f(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
上面的代码可以看出:
- 关键字 func 用来声明一个函数 f
- 函数可以有一个或者多个参数,每个参数后面带有类型,通过 , 分隔
- 函数可以返回多个值
- 上面返回值声明了两个变量 output1 和 output2 ,如果不想声明也可以,直接就两个类型
- 如果只有一个返回值且不声明返回值变量,那么可以省略 包括返回值的括号
- 如果没有返回值,那么就直接省略最后的返回信息
- 如果有返回值, 那么必须在函数的外层添加return语句
下面来看一个实际应用函数的例子(用来计算Max值):
package main
import "fmt"
// 返回a和b中最大值.
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) //调用函数max(x, y)
max_xz := max(x, z) //调用函数max(x, z)
fmt.Printf("max(%d, %d) = %d
", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d
", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d
", y, z, max(y,z)) // 也可在这直接调用它
}
上面这个里面可以看到 max 函数有两个参数,它们的类型都是 int ,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时注意到它的返回值就是一个类型,这个就是省略写法。
多个返回值
Go语言比C更先进的特性,其中一点就是函数能够返回多个值。
直接看例子:
package main
import "fmt"
//返回 A+B 和 A*B
func f(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
res1, res2 := f(x, y)
fmt.Printf("%d + %d = %d
", x, y, res1)
fmt.Printf("%d * %d = %d
", x, y, res2)
}
上面的例子可以看到直接返回了两个参数,当然也可以命名返回参数的变量,这个例子里面只是用了两个类型,也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。
func SumAndProduct(A, B int) (res1 int, res2 int) {
res1 = A+B
res2 = A*B
return
}
变参
Go函数支持变参。
接受变参的函数是有着不定数量的参数的。
为了做到这点,首先需要定义函数使其接受变参:
func f(arg ...int) {}
arg …int 告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是 int 。在函数体中,
变量 arg 是一个 int 的 slice :
for _, n := range arg {
fmt.Printf("%d
", n)
}
传值与传指针
传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
为了验证上面的说法,来看一个例子:
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a int) int {
a = a+1 // 改变了a的值
return a //返回一个新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(x) //调用add1(x)
fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4"
fmt.Println("x = ", x) // 应该输出"x = 3"
}
虽然调用了 add1 函数,并且在 add1 中执行 a = a+1 操作,但是上面例子中 x 变量的值没有发生变化。
理由很简单:因为当调用 add1 的时候, add1 接收的参数其实是 x 的copy,而不是 x 本身。
如果真的需要传这个 x 本身,该怎么办呢?
这就牵扯到了所谓的指针。
变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。
只有 add1 函数知道 x 变量所在的地址,才能修改 x 变量的值。
所以需要将 x 所在地址 &x 传入函数,并将函数的参数的类型由 int 改为 *int ,即改为指针类型,才能在函数中修改 x 变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。
请看下面的例子:
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}
这样,就达到了修改 x 的目的。
那么到底传指针有什么好处呢?
传指针使得多个函数能操作同一个对象。
传指针比较轻量级 (8bytes),只是传内存地址,可以用指针传递体积大的结构体。
如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当要传递大的结构体的时候,用指针是一个明智的选择。
Go语言中 channel , slice , map 这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变 slice 的长度,则仍需要取地址传递指针)
defer
Go语言中有种不错的设计,即延迟(defer)语句,可以在函数中添加多个 defer 语句。
当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。
特别是当进行一些打开资源的操作时,遇到错误需要提前返回,在返回前需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,一般写打开一个资源是这样操作的:
func ReadWrite() bool {
file.Open("file")
// 做一些工作
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
上面有很多重复的代码,Go的 defer 有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。
在 defer 后指定的函数会在函数退出前调用。
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
如果有很多调用 defer ,那么 defer 是采用后进先出模式,所以如下代码会输出:
4 3 2 1 0
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
通常来说,defer 会用在释放数据库连接,关闭文件等需要在函数结束时处理的操作。