第5章 函数(function)
Go语言支持普通函数、匿名函数和闭包 Go语言的函数属于“一等公民”(first-class),也就是说:
-
函数本身可以作为值进行传递
-
支持匿名函数和闭包(closure)
-
函数可以满足接口
5.1 声明函数
普通函数需要先声明才能调用
函数声明包括参数和函数名等,这样编译器通过声明才能知道函数时如何在调用代码和函数体之间的传递参数和返回参数
5.1.1 普通函数的声明声明
格式
func 函数名(参数列表) (返回参数列表) {
函数体
}
函数名:
-
有字母、数字、下划线组成。
-
函数名开头首字母必能为数字
-
同个包内,函数名不能重复
参数列表:
-
参数有参数变量和参数类型组成 ,如 func name( a int, b string)
返回参数列表:
-
可以是返回值类型列表,可以是类似参数类型列表中变量名和类型名的组合
-
有返回值时,必须使用return语句提供返回值列表
函数体:能够被重复调用的代码片段
5.1.2 参数类型的简写
参数列表多类型,使用逗号
隔开
同类型变量,省略前面变量类型,仅保留最后一个变量类型即可
func add (a, b int) int { return a + b }
5.1.3 函数的返回值
Go语言支持多返回值,常使用返回值的最后一个返回值最后判断函数执行返回可能的错误
conn, err := connectToNetwork() // err 就是用来接收返回错误
提示:
c/c++只支持一个返回值,需要返回对个数值时,需要使用结构体
返回结果。
返回值种类:
-
同一种类型返回值
返回的多个返回值类型是相同的,用括号
括起来,返回值之间用逗号
隔开
func values() (int, int) { return 1, 2 }
-
带变量名的返回值
Go语言支持对返回值命名,不同类型默认值:数组为0、字符串为空字符串、布尔为false、指针为nil等。
func values() (a, b int) { a = 1 b = 2 return } // 另一种方式 func values() (a, b int) { a = 1 return a, 2 }
注意:两种返回值形式只能二选一,混合可能会报错
5.1.4 调用函数
函数的局部变量只能在函数体中使用,函数调用结束后,这些变量都被释放并且失效
格式
返回值变量列表 = 函数名(参数列表)
-
函数名:调用函数名称
-
参数列表:参数变量以逗号隔开,尾部无须分号结尾
-
返回值变量列表:多个返回值使用逗号分隔
示例:
res := add(1, 1)
5.1.5 示例:时间“秒”转换时间单位
package main import ( "fmt" ) // 定义 分、时、天 const ( SecondsPerMinute = 60 SecondsPerHour = SecondsPerMinute * 60 SecondsPerDay = SecondsPerHour * 24 ) // 将秒转为分、时、天 func SecondsToOtherTime(seconds int) (minute int, hour int, day int) { day = seconds / SecondsPerDay hour = seconds / SecondsPerHour minute = seconds / SecondsPerMinute return } func main() { // 将秒解析为时间单位 fmt.Println(SecondsToOtherTime(2500)) // 41 0 0 // 获取消息和分钟 _, hour, day := SecondsToOtherTime(2500) fmt.Println(hour, day) // 0 0 // 获取分 minute, _, _ := SecondsToOtherTime(2500) fmt.Println(minute) // 41 }
5.4匿名函数
5.4.1 定义
格式:
func (函数列表) (返回参数列表) {
函数体
}
1.定义时调用匿名函数、
func (data int) { fmt.Println("hello", data) }(100) //hello 100
-
将匿名函数赋值到变量
f := func (data int) { fmt.Println("hello1", data) } f(50)
5.4.2 匿名函数作为回调函数
package main import ( "fmt" ) // 定义遍历函数 func visit(list []int, f func(int)) { for _, v := range list { f(v) } } func main() { // 匿名函数作为回调函数 visit([]int{1,2,3}, func(v int){ fmt.Println(v) }) }
5.4.3 使用匿名函数实现操作封装
package main import ( "fmt" "flag" ) var skillParam = flag.String("skill", "", "skill to perform") func main() { flag.Parse() var skill = map[string]func(){ "fire": func() { fmt.Println("chicken fire") }, "run": func() { fmt.Println("soldier run") }, "fly": func() { fmt.Println("angel fly") }, } if f, ok := skill[*skillParam]; ok { f() } else { fmt.Println("skill not found") } } go run 5.4.3\ skillParam.go --skill=fly angel fly
5.5 函数类型实现接口-- 把函数作为接口来调用
5.5.1 结构体实现接口
结构体声明一个方法 实例化结构体 然后结构体复制到接口变量 通过接口变量调用定义的方法(方法为要实现的接口方法)
package main import ( "fmt" ) // 结构体类型 type Struct struct { } // 实现结构体 func (s * Struct) Call(p interface{}) { fmt.Println("from struce", p) } type Invoker interface { Call(interface{}) } func main() { // 声明接口变量 var invoker Invoker //实例化结构体 s := new(Struct) //实例的结构体复制到接口 invoker = s // 使用接口调用实例化结构体到方法 Struct.Call invoker.Call("Hello") }
结果: $ go run ./5.5/5.5.1\ func_implinterface.go from struce Hello
5.5.2 函数体实现接口
函数不能直接实现接口
-
需要将函数定义为类型,通过类型实现结构体
-
当类型方法被调用时,还需要函数本体
package main import ( "fmt" ) // 函数定义为类型 type FuncCaller func(interface{}) // 实现Invoker的Call func (f FuncCaller) Call(p interface{}) { // 调用f() 函数本体 f(p) } type Invoker interface { Call(interface{}) } func main() { // 声明接口变量 var invoker Invoker // 将匿名函数转微FuncCaller类型,在赋值给接口 invoker = FuncCaller(func (v interface{}) { fmt.Println("from function", v) }) // 使用接口调用FuncCaller.Call 内部会调用函数实体 invoker.Call("hello func") }
5.6 闭包(Closure) -- 引用外部变量的匿名函数
5.6.1 闭包内部修改引用变量
package main import ( "fmt" ) func main() { str := "Hello world" // 创建匿名函数 foo := func() { // 匿名函数中访问str str = "hello go" } //调用匿名函数 foo() fmt.Println(str) }
结果:hello go
5.6.2 示例:闭包的记忆效应
可以应用实现类似设计模型中工厂模式的生成器
package main import ( "fmt" ) // 提供一个值,没次调用函数会指定对值累加 func Accumulate(value int) func() int { // 返回一个闭包 return func() int { // 累加 value ++ return value } } func main() { // 创建一个累计器,初始值为1 accumulator := Accumulate(1) fmt.Println(accumulator()) fmt.Println(accumulator()) // 打印累加器的函数地址 fmt.Printf("%p\n", accumulator) // 创建一个累加器,初始值为1 accumulator2 := Accumulate(2) fmt.Println(accumulator2()) fmt.Printf("%p\n", accumulator2) }
输出结果:
2
3
0x1094e30
3
0x1094e30
5.7 可变参数
GO支持可变参数特性
函数声明和调用没有固定数量的参数 格式如下:
func 函数名(固定参数列表, v ...T) (返回参数列表) {
函数体
}
5.7.1 fmt包中的例子
两种方式:
-
所有参数都是可变参数
-
部分参数可变参数
所有参数都是可变参数:f m t.Println
func Println(a ...interface{}) (n int,err error) { return Eprintln(os.Stdout, a...) }
部分参数可变参数:fmt.Printf
func Printf(fomart string, a ...interface{}) (n int, err error) { return Eprintf(os.Stdout, format, a...) }
5.7.2 遍历可变参数列表
可变参数是一个切片,若需要获取每个参数,可以对可变参数变量进行遍历
package main import ( "fmt" "bytes" ) // 定义一个函数,函数0~n,类型约束为字符串 func joinStrings(slist ...string) string { //定义一个字节缓冲,快速连接字符串 var b bytes.Buffer //遍历可变参数slist 类型为 []string for _, s := range slist { // 遍历出字符串连续写入字节数组 b.WriteString(s) } return b.String() } func main() { fmt.Println( joinStrings("pig ", "and", " rat") ) fmt.Println( joinStrings("hammer", " mom", " and", " hawk")) }
$ go run 5.7/aviable.go pig and rat hammer mom and hawk
5.7.3 获取可变参数类型
当可变参数为interface{}类型时,可传入任何类型的值
package main import ( "fmt" "bytes" ) // 定义一个函数,参数为interface{} func printTypeValue(slist ...interface{}) string { //定义一个字节缓冲,快速连接字符串 var b bytes.Buffer //遍历可变参数 for _, s := range slist { // 将interface{}类型格式化为字符串 str := fmt.Sprintf(" %v", s) //类型描述 var typeString string //对s进行类型断言 switch s.(type) { case bool: typeString = "bool" case string: typeString = "string" case int: typeString = "int" } //写字符串前缀 b.WriteString("value: ") //写入值 b.WriteString(str) //写入类型前缀 b.WriteString(" type: ") //写入类型字符串 b.WriteString(typeString) b.WriteString("\n") } return b.String() } func main() { //将不同类型对变量通过printTypeValue()打印处来 fmt.Println( printTypeValue(100, "str", true) ) }
$ go run 5.7/printtypevalue.go 5.7/aviable.go value: 100 type: int value: str type: string value: true type: bool
5.74 在多个可变参数中传递参数
可变参数是一个包含参数的切片,如果多个可变参数中传递参数,可以在传递时可变参数中默认添加“...”在可变参数后
,代表将切片中的元素进行传递,而不为传递可变参数变量本身
package main import "fmt" // 实际打印的函数 func rawPrintf(rawList ...interface{}){ //遍历可变参数的切片 for _, s := range rawList { //打印参数 fmt.Println(s) } } // 打印函数封装 func print(slist ...interface{}) { //将slist可变参数切片完整传递下个函数 rawPrintf(slist...) } func main() { print(1, 2, 3) }
结果:1,2,3
5.8 延迟执行语句(defer)
Go语言defer语句时在其他语句执行后在处理
在defer归属的函数即将返回时,延迟处理的语句时按照defer的倒序进行执行的(先被defer的语句最后执行,最后defer语句最先被执行)
5.8.1 多个延迟执行语句的处理顺序
package main import ( "fmt" ) func main() { fmt.Println("defer begin") //将defer放入延迟调用栈 defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) //最后defer,最先调用 fmt.Println("defer end") }
$ go run 5.8/defer1.go defer begin defer end 3 2 1
小结:就是先执行函数里面的非defer语句,然后再把defer语句最后定义的先执行处理
5.8.2 使用延迟语句在函数退出时释放资源
-
使用延迟并发解锁
函数中并发使用map, 为防止竞态,使用sync.Mutex加锁
var ( valueByKey = make(map[string]int) //保证使用映射时的并发安全的互斥锁 valueByKeyGuard sync.Mutex ) func readValue(key string) int { // 对共享资源加锁 valueByKeyGuard.Lock() //取值 v := valueByKey[key] //对共享资源解锁 valueByKeyGuard.Unlock() return v }
使用defer语句调整
func readValue(key string) int { // 对共享资源加锁 valueByKeyGuard.Lock() //对共享资源解锁,延迟到函数结束调用 defer valueByKeyGuard.Unlock() return valueByKey[key] }
2.使用延迟释放句柄
文件操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程
操作完后,如果不关闭文件资源,进程将一直无法释放文件资源
func fileSize(filename string) int64 { //根据文件名打开文件,返回文件局柄和错误 f, err := os.Open(filename) //如果打开错误,返回文件大小为0 if err != nil { return 0 } // 取文件状态信息 info, err := f.Stat() //文件信息错误,关闭文件,返回文件大小为0 if err != nil { f.Close() return 0 } //取文件大小 size := info.Size() //关闭文件 f.Close() return size }
Defer 语句调整
func fileSize(filename string) int64 { //根据文件名打开文件,返回文件局柄和错误 f, err := os.Open(filename) //如果打开错误,返回文件大小为0 if err != nil { return 0 } // 延迟调用Close defer f.Close() // 取文件状态信息 info, err := f.Stat() //文件信息错误,关闭文件,返回文件大小为0 if err != nil { return 0 } //取文件大小 size := info.Size() return size }
5.9 运行时错误处理
Go语言在错误处理设计有两个特征:
-
一个可能造成错误的函数,需要返回值中返回一个错误接口(error),成功则返回ni l,否则返回错误
-
函数调用后需检查错误,若错误,需进行错误处理
Go语言没有异常处理机制,Go不主张,设计这觉得异常机制被过度使用
Go 语言希望错误处理视为正常开发环节
5.9.1 net包中的例子
net.Dial() go语言系统包net中的一个函数,一般用于创建一个socket连接
net.Dial() 返回两个值,即Conn和 error; socket操作返回Conn连接对象和error, 错误,error返回错误类型,Conn会返回空
func Dial(network, address string) (Conn, errror) {
var d Dialer
return d.Dial(newtwork, address)
}
在io包中Writer接口也拥有错误返回,如下:
type Writer interface {
Write(p []byte) (n int, err error)
}
io包中还有另一个Closer接口,只是返回一个错误,如:
type Closer interface {
Close() error
}
5.9.2 错误接口的定义格式
error 是Go系统声明的接口类型,格式:
type error interface { Error() string }
所有符合Error() string 格式的方法,都能实现错误接口
5.9.3 自定义一个错误
Go语言使用errors包进行错误定义,返回错误前,需要定义会产生哪些可能的错误
var error = errors.New('this is an error')
注意:一般在包作用域
声明,尽量减少在使用直接使用errors.New返回
-
errors包对New () 的定义
// 创建错误类型 func New(text string) error { return &errorString{text} } // 错误字符串 type errorString struct{ s string } //返回发送何种错误 func (e *errorString) Error() string { return e.s }
-
在代码中使用错误定义
package main import ( "fmt" "errors" ) // 定义除数为0的错误 var errDivisionByZero = errors.New("division by zero") func div(dividend, divisor int) (int, error) { //判断除数为0的情况返回 if divisor == 0 { return 0, errDivisionByZero } return dividend / divisor, nil } func main() { fmt.Println(div(1, 0)) }
$ go run 5.9/errors.go 0 division by zero
5.9.4 示例:在解析中使用自定义错误
package main import ( "fmt" ) //声明一个解析错误 type ParseError struct { Filename string // 文件名 Line int// 行号 } //实现error接口,返回错误信息 func (e *ParseError) Error() string { return fmt.Sprintf("%s:%d", e.Filename, e.Line) } // 创建解析错误 func newParseError(filename string, line int) error { return &ParseError{filename, line} } func main() { var e error //创建一个错误,包含文件名和行 e = newParseError("main.go", 1) //通过error接口查看错误 fmt.Println(e.Error()) //根据错误接口的具体类型,获取详情的错误信息 switch detail := e.(type) { case *ParseError: fmt.Printf("Filename: %s Line: %d \n", detail.Filename, detail.Line) default: fmt.Println("other error") } }
$ go run 5.9/*
main.go:1 Filename: main.go Line: 1
5.10 宕机(panic)--程序终止运行
5.10.1 手动出发宕机
panic() 函数触发宕机
package main func main() { panic("crash") } $ go run 5.10/panic.go panic: crash goroutine 1 [running]: main.main() .../projects/goes/5.10/panic.go:5 +0x39 exit status 2
5.10.2 在运行依赖必备资源缺失时主动出发宕机
regexp 是Go语言的正则表达式包,需要编译正确才能使用,而且编译必须成功
编译有两种函数:
-
func Compile(exr string) (*Regexp, error)
-
func MustComiple(str string) *Regexp
func MustCompile (str string) *Regexp { regrep, error := Compile(str) if error != nil { panic(`regrexp: Compile(` + quote(str) + `):` + error.Error()) return regexp } }
5.10.3 在宕机时出发延时执行语句
package main import ( "fmt" ) func main() { defer fmt.Println("宕机后要做的事情1") defer fmt.Println("宕机后要做的事情2") panic("宕机") } $ go run 5.10/panic.go 宕机后要做的事情2 宕机后要做的事情1 panic: 宕机 goroutine 1 [running]: main.main() ...projects/goes/5.10/panic.go:11 +0xf1 exit status 2
go语言没有异常处理机制,使用pa nic 出发宕机类似其他语言的抛出异常
recover 的宕机恢复机制类似try/catch机制
5.11 宕机恢复(recover)--防止程序崩溃
5.11.1 让程序崩溃时继续执行
package main import ( "fmt" "runtime" ) // 崩溃时需要传递上下文信息 type panicContext struct { function string //所在函数 } //保护方式允许一个函数 func ProtectRun(entry func()) { defer func () { // 宕机时,获取panic传递上下文并打印 err := recover() switch err.(type) { case runtime.Error: fmt.Println("runtime error:", err) default: fmt.Println("error:", err) } }() entry() } func main() { fmt.Println("运行前") // 手动触发错误 ProtectRun(func() { fmt.Println("手动宕机前") // 使用panic传递上下文 panic(&panicContext{ "手动出发panic", }) fmt.Println("手动宕机后") }) // 故意造成空指针错误 ProtectRun(func() { fmt.Println("赋值宕机前") var a *int *a = 1 // 模拟代码中的空指针赋值造成的错误 fmt.Println("赋值宕机后") }) fmt.Println("运行后") }
$ go run 5.11/protectrun.go 运行前
5.11.2 panic 和recover 的关系
特点:
-
有panic 没recover ,程序宕机
-