一、函数
1.1 什么是函数
函数是执行特定任务的代码块。
关键字func用于定义函数。go中函数特点:
- 无须前置声明
- 不支持命名嵌套定义
- 不支持同名函数重载
- 不支持默认参数
- 支持不定长变参
- 支持多返回值
- 支持命名返回值
- 支持匿名函数和闭包。
1.2 参数传递
go语言函数的参数也是存在值传递和引用传递
函数运用场景
值传递
package main
import (
"fmt"
"math"
)
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。
引用传递
引用传递其实是一个伪命题。因为严格来说go中并没有引用传递,这么说只是为了便于理解。
因为不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
请看下面的例子
func test(x *int){
fmt.Printf("pointer: %p, target: %v\n", &x, x) //输出形参x的地址
}
func main() {
a:=0x100
p:=&a
fmt.Printf("pointer: %p, target: %v\n",&p,p) //输出实参p的地址
test(p)
}
输出:
pointer: 0xc042004028, target: 0xc042008098
pointer: 0xc042004038, target: 0xc042008098
从结果可以看出,虽然形参和实参都指向同一目标(值相同,指向同一内存地址),但指针本身的地址&p不同,说明传递指针时依然是被复制,即所谓的没有引用传递。
//简单的一个函数,实现了参数+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"
}
- 传指针使得多个函数能操作同一个对象。
- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
-
Go语言中string, slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
上句源于《Go Web编程》一书,但严格来说string其实在底层实现上是引用类型,但是因为string不允许修改,只能生成新的对象,在逻辑上和值类型无差别。
从表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
其实在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也是提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好。
下面是一个指针参数导致实参变量被分配到堆上的简单示例。可对比传值参数的汇编代码,从中可看出具体的差别。
func test(p *int){
go func() { //延长p的生命周期
println(p)
}()
}
func main() {
x:=100
p:=&x
test(p)
}
go tool objdump -s "main\.main" 堆分配.go //输出编译器优化策略
.\堆分配.go:11:5: &x escapes to heap //逃逸
.\堆分配.go:10:5: moved to heap: x
go tool objdump -s "main\.main" 4.函数.exe
...
... CALL runtime.newobject(SB) //在堆上为x分配内存
... CALL main.test(SB)
要实现传出参数(out),通常建议使用返回值。当然,也可以继续用二级指针。
func test2(p **int){
x:=100
*p = &x
}
func main() {
var p *int
test2(&p)
println(*p)
}
输出:100
注意这个例子的巧妙之处,test2实际上是把&p=&x了,这样没有返回值也实现了返回的效果。
如何函数参数过多,建议将其重构为一个符合结构类型,也算是变相实现可选参数和命名实参功能。
type serverOption struct {
address string
port int
path string
timeout time.Duration
log *log.Logger
}
func newOption() *serverOption {
return &serverOption{ //默认参数
address: "0.0.0.0",
port: 8080,
path: "/var/test",
timeout: time.Second * 5,
log: nil,
}
}
func server(option *serverOption){}
func main() {
opt:=newOption()
opt.port=9000 //命名参数设置
server(opt)
}
1.3 可变参
Go对参数的处理偏向保守,不支持默认值的可选参数,不支持命名实参。
但支持变参,变参本质上就是一个切片,只能接受一到多个同类型参数,且必须放在列表末尾。
func testargs( s string, a ...int){
fmt.Printf("%T,%v",a,a)
}
func main() {
testargs("abc",1,2,3,4)
}
输出:
[]int,[1 2 3 4]
将切片作为变参时,必须对其进行展开操作。如果是数组,先将其转换为切片。
func testargs( s string, a ...int){
fmt.Printf("%T,%v",a,a)
}
func main() {
a:=[3]int{10,20,30}
testargs("abc",a[:]...) //转换为slice后展开
}
既然变参时切片,那么参数赋值的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数copy复制底层数据。
func testargs( s string, a ...int){
for i:=range a{
a[i]+=100
}
}
func main() {
a:=[3]int{10,20,30}
testargs("abc",a[:]...) //转换为slice后展开
fmt.Println(a)
}
输出:[110 120 130]
1.4 返回值
一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Mahesh", "Kumar")
fmt.Println(a, b)
}
稍有不便的是没有元组(tuple)类型,也不能用数组、切片接收返回值,但可用_忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当作结果直接返回。
func div(x,y int)(int,error){
if y==0{
return 0,errors.New("division by zero")
}
return x /y, nil
}
func log(x int, err error){
fmt.Println(x,err)
}
func testdiv()(int,error){
return div(5,0)
}
func main(){
log(testdiv())
}
func eval(a,b int,op string) (int,error){
switch op {
case "+":
return a+b,nil
case "-":
return a-b,nil
case "*":
return a*b,nil
case "/":
return a/b,nil
default:
return 0,fmt.Errorf("unsupported op:%s",op)
}
}
命名返回值
对返回值命名和简短变量定义一样,优缺点共存。
func paging(sql string, index int)(count int, pages int, err error){}
从上面这个简单示例可以看出,命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示。
命名返回值和参数一样,可当做函数局部变量使用,最后由return隐式返回值。
func div1(x, y int)(z int, err error){
if y==0{
err = errors.New("div by zero")
return //隐式返回z=0, err="div by zero"
}
z = x/y
return //隐式返回z , nil
}
这些特殊的“局部变量”会被不同层级的同名变量遮蔽。好在编译器能检查到此类状况,只要改为显式return即可。
如果返回值类型能明确表明其含义,就尽量不要对其命名。
func NewUser()(*User,error)
1.5 空白标识符
_是Go中的空白标识符。它可以代替任何类型的任何值。让我们看看这个空白标识符的用法。
比如rectProps函数返回的结果是面积和周长,如果我们只要面积,不要周长,就可以使用空白标识符。
package main
import (
"fmt"
)
func rectProps(length, width float64) (float64, float64) {
var area = length * width
var perimeter = (length + width) * 2
return area, perimeter
}
func main() {
area, _ := rectProps(10.8, 5.6) // perimeter is discarded
fmt.Printf("Area %f ", area)
}
1.6 闭包
再提到闭包前,必须要先介绍下匿名函数
匿名函数是指没有定义名字符号的函数,是一个"内联"语句或表达式。
除了没有名字外,匿名函数和普通函数最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套结果。匿名函数可直接调用,保存到变量,作为参数或返回值。
匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
匿名函数
赋值给变量:
func main(){
add:=func(x,y int) int{
return x+y
}
println(add(1,2))
}
作为参数:
func ttest( f func()){
f()
}
func main(){
ttest(func() {
println("hello world")
})
}
作为返回值:
func ttest()func(m,n int)int{
return func(x,y int)int{
return x+y
}
}
func main(){
add:=ttest()
println(add(1,2))
}
将匿名函数赋值给变量,与为普通函数提供名字标记符有着根本的区别。当然,编译器会为匿名函数生成一个“随机”符号名。
普通函数和匿名函数都可作为结构体字段,或经通道传递。
func testStruct(){
type calc struct {
mul func(x,y int) int
}
x:=calc{
mul:func(x, y int) int {
return x * y
},
}
println(x.mul(2,3))
}
func testChannel(){
c:=make(chan func(int,int)int ,2) // 2是双向通道?
c<-func(x,y int)int{
return x+y
}
println((<-c)(7,2)) //注意下这个,形式很特别
}
func main(){
testStruct()
testChannel()
}
除闭包因素外,匿名函数也是一种常见重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码层次。
闭包
闭包是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。
func testClosure(x int)func(){
return func() {
println(x)
}
}
func main() {
f:=testClosure(111)
f()
}
就这段代码而言,test返回的匿名函数会引用上下文环境变量x。 当该函数在main中执行时,它依然可正确读取x的值,这种现象称作闭包。
闭包是如何实现的?匿名函数被返回后,为何还能读取环境变量的值?修改一下代码再看。
func testClosure(x int)func(){
println(&x)
return func() {
println(&x,x)
}
}
func main() {
f:=testClosure(0x100)
f()
}
输出:
0xc04203e000
0xc04203e000 256
通过输出指针,我们注意到闭包直接饮用额原环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切。
本质上返回的是一个funcval结构,可在runtime/runtime2.go中找到相关定义。
go build -gcflags "-N -L"
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性。
func testCC()[]func(){
var s []func()
for i:=0;i<3;i++{
s=append(s, func() {
fmt.Println(&i,i)
})
}
return s
}
func main(){
for _,f:=range(testCC()){
f()
}
}
输出:
0xc042008098 3
0xc042008098 3
0xc042008098 3
对这个结果不必惊讶。很简单,for循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当main执行这些函数时,他们读取的是环境变量i最后一次循环时的值。
解决方法就是每次用不同的环境变量或传参赋值,让各自闭包环境各不相同。
func testCC()[]func(){
var s []func()
for i:=0;i<3;i++{
x:=i //每次循环都重新定义
s=append(s, func() {
fmt.Println(&x,x)
})
}
return s
}
func main(){
for _,f:=range(testCC()){
f()
}
}
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要做同步处理。
func testcs(x int)(func(),func()){
return func() {
println(x)
x+=10
}, func() {
println(x)
}
}
func main(){
a,b:=testcs(100)
a()
b()
}
闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。对于性能要求较高的场合,须慎重使用。
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
结果
1
2
3
1
2
函数做为值
在Go中函数也是一种变量,我们可以通过type来定义它
同种方法:参数类型、个数、顺序相同,返回值相同
package main
import "fmt"
type testInt func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println("Even elements of slice are: ", even)
}
type testInt func(int) bool
就是将该种函数类型赋值给testInt
一般步骤:
- 定义一个函数类型
- 实现定义的函数类型
- 作为参数调用
有点接口的感觉
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活
1.7 Panic和Recver
Panic和Recover
Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?
func panic(v interface{})
func recover() interface{}
Panic
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中
断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产
生。也可以由运行时错误产生,例如访问越界的数组。
Recover
是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。
下面这个函数演示了如何在过程中使用panic
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
下面这个函数检查作为其参数的函数在执行时是否会产生panic:
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
return
}
1.8 defer
defer向当前函数注册稍后执行的函数调用。这些调用被称为延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作。
注意,延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。如对状态敏感,可改用指针或闭包。下面这个例子,说明用闭包时,变量跟着全局改动而改,但参数不变。
func main() {
x,y:=1,2
defer func(a int) {
fmt.Println("defer x,y",a,y) //对y进行闭包调用
}(x) //注册时复制调用参数
x+=100 //对x的修改不会影响延迟调用
y+=100
fmt.Println(x,y)
}
输出:
101 102
defer x,y 1 102
延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃。
多个延迟注册按FILO次序执行:
func main(){
defer println("a")
defer println("b")
输出:
b
a
编译器通过插入额外指令来实现延迟调用执行,而return和panic都会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,它会先更新返回值。
func main() {
fmt.Println("testdefer: ", testdefer())
}
func testdefer()(z int){
fmt.Println("prt:", z)
defer func() {
fmt.Println("defer:",z)
z+=100 // 修改命名返回值
}()
return 100 //实际执行次序: z= 100, call defer, ret
}
输出:
prt: 0
defer: 100
testdefer: 200
误用
千万记住,延迟调用在在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
案例:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长。
func main() {
for i:=0; i<10;i++{
path:=fmt.Sprintf("./log/%d.txt",i)
fmt.Println(path)
f,err:=os.Open(path)
if err!=nil{
log.Println(err)
continue
}
defer f.Close() // 这个关闭操作在main函数结束时才执行,而不是当前循环中执行
}
}
应该直接调用,或重构为函数,将循环和处理算法分离。
func main() {
do := func(n int) {
path:=fmt.Sprintf("./log/%d.txt",n)
f,err:=os.Open(path)
if err!=nil{
log.Println(err)
continue
}
defer f.Close() // 这个关闭操作在main函数结束时才执行,而不是当前循环中执行
}
for i:=0; i<10;i++{
do(i)
}
}
网友评论