Golang_4_函数
四、函数
4.1 定义
使用 func
定义函数,特性如下:
- 无须前置声明
- 不支持同名函数重载
- 不支持命名嵌套 nested – ?
- 不支持默认参数
- 不支持定长变参
- ==支持多返回值==
- 支持命名返回值
- 支持匿名函数和闭包
函数属于第一类对象,具备相同签名(参数和返回值一样)的视为同一类型
第一类对象:可在运行期创建,可作函数参数或返回值,可存入变量的实体。最常见的就是匿名函数
func hello() {
fmt.Println("hello world!")
}
// exec函数,参数值为函数名称
func exec(f func()) {
f()
}
func main(){
f := hello
exec(f)
}
函数只能判断是否 ==nil
,其余操作不支持
建议命名
- 动词介词加名词
scanWords
- 避免不必要的缩写
printError
优于printErr
- 避免使用类型关键字
buildUserStruct
看上去很别扭 - 避免歧义
- 避免只能通过大小写来区分的同名函数
- 避免使用内置函数名
- 避免使用数字,除非是
UTF8
这种专有名词 - 避免添加作用域提示前缀
- 统一使用 camel/pascal case 拼写风格 (驼峰)
- 使用相同术语保持一致性
- 使用习惯语,比如
init
初始化,is/has
返回布尔值 - 使用反义词组命名行为相反的函数,
get/set
、min/max
4.2 参数
- 形参:函数定义中的参数
- 类似局部变量
- 实参:函数调用时所传递的参数
- 函数外部对象,可以是常量、变量、表达式或函数等
==Golang是值传递==,也称作拷贝传递(pass-by-value),函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存
func test(x *int){
fmt.Printf("point: %p, target: %v\n", &x, x)
}
func main() {
a := 0x100
p := &a
fmt.Printf("point: %p, target: %v\n", &p , p)
// p 的地址和 p 的 地址
//point: 0xc42008a018, target: 0xc420082008
test(p)
// 形参的地址 和 传入的值
//point: 0xc42008a028, target: 0xc420082008
}
尽管实参和形参都指向同一目标,但传递指针时依然被赋值
使用指针更好,在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快很多,并发编程也提倡尽可能使用不可变对象(只读或复制),可以消除数据同步p64==
参数的传出有两种方式,通常建议使用返回值,也可以使用二级指针
func test(p **int) {
x := 100
*p = &x // 地址操作
}
func main() {
var p *int // 定义指针变量
test(&p) // 调用函数,使用 &p
fmt.Println(*p) // 还原指针对应的值,输出100
}
变参
变参实质上就是切片。只能收到一个或多个同类型参数,必须放在列表尾,写法为...
。
func test(s string, a ...int) {
fmt.Printf("%T, %v\n", s, s)
fmt.Printf("%T, %v\n", a, a)
}
func main() {
test("abc", 1, 2, 3, 4)
}
// string, abc
// []int, [1 2 3 4]
用切片调用变参输入函数时,需要进行展开操作。若是数组,需要转化为切片。
func test(a ...int) {
fmt.Printf("%T, %v\n", a, a)
}
func main() {
a := [3]int{1, 2, 3}
test(a[:]...) // 数组转化为切片类型
// []int, [1 2 3]
}
变参复制的是切片自身,不包括底层数组,可以修改数据
func test(a ...int) {
for i := range a{
a[i] += 100
}
}
func main() {
a := []int{1, 2, 3} // 切片类型
test(a...) // 切片类型传参
fmt.Println(a)
}
4.3 返回值
有返回值的函数必须得有明确的return
终止语句
func test(x int) int{
if x > 0 {
return 1
} else if x < 0 {
return -1
}
}// 错误:Missing return at end of function
除非有panic
或者无break
的死循环,则无须return
终止语句
func test(x int) int{
for{
x++
}
}
// 死循环 without break,不报错
// 若循环内加上 break,则会报错
由于可以返回多值,error
模式得以返回多状态
import "errors"
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)
}
// 多返回值直接作 return 的结果
func test() (int, error) {
return div(5, 0)
}
func main() {
// 多返回值作实参
log(test())
}
命名返回值
func paging(sql string, index int) (count int, pages int, err error) {
return // 隐式返回,相当于 return count, pages, err
}
会有两个问题
-
遮蔽
命名返回值和参数一样,相当于函数的局部变量,最后由
return
隐式返回,这种 局部变量 会被不同级的同名变量遮蔽,需要显式得返回func add(x, y int) (z int) { { z := x + y return z // 必须显式返回,否则报错 } return }
-
所有返回值都要命名(如有命名)
所有返回值都要命名,否则编译器会搞不清情况,编译器会跳过未命名的返回值,无法准确匹配
如果返回值类型可以明确表明含义,就尽量不要命名
func test(str string) (int, s string, e error) { return 0, str, errors.New("123")// 编译器忽略了 int 返回值,所以第一个是 string ,不能为 int return str, errors.New("123") // 但是写两个返回值,又会报错 “Not enough argument to return” }
4.4 匿名函数
除了没有名字外,匿名函数和普通函数完全相同。
最大的区别在于:可以在函数内部定义匿名函数,形成嵌套效果。
匿名函数可以直接调用、保存到变量、作为参数或返回值
-
直接执行
func main(){ func(s string){ println(s) }("hello world") }
-
赋值给变量
func main(){ add := func(x, y int) int{ return x + y } fmt.Println(add(1, 2)) }
-
作为参数
// 函数类型参数 func test(f func()) { f() } func main(){ test(func() { fmt.Println("hello world") }) }
-
作为返回值
// 函数类型参数 func test() func(int, int) int { return func(x, y int) int { return x + y } } func main(){ add := test() fmt.Println(add(1, 2)) }
普通函数和匿名函数都可以作为结构体字段,或经过通道传递 ==p70==
未使用的匿名函数会被编译器当作错误
匿名函数也是一种常见的重构手段 ==p71==
闭包
闭包 = 函数 + 引用环境
// 定义一个函数,它的返回值是一个函数
func test(x string) func() {
name := "小王子"
return func() {
fmt.Println(x, name)
}
}
// 闭包 = 函数 + 外层变量的引用
func main() {
f := test("hello") // f 就是一个闭包
f() // 相当于执行了test函数内部的匿名函数
}
test
返回的是匿名函数,这个匿名函数包含它自己上下文环境变量 x 和 name 的引用。
当该函数在main中执行时,它依然可以正确读取x 和 name 的值,这种现象称为闭包
匿名函数被返回后,还能读取环境变量的值
例一:
// 1. 声明一个 makeSuffixFunc 的函数,参数为 suffix(string)
// 2. 返回值是一个函数类型:这个函数类型接收一个string参数,返回一个string参数
func makeSuffixFunc(suffix string) func(string) string {
// 3. 返回一个匿名函数,和上层函数的返回值相同
return func(name string) string {
// HasSuffix:返回 name 是否以 suffix 结尾
// 4. name 是匿名函数的变量,suffix 不是匿名函数的变量,而是外层函数的变量
// 5. 定义的不是一个普通的匿名函数,还包含外层变量的引用,返回的结果也是一个闭包
if !strings.HasSuffix(name, suffix){
return name + suffix
}
return name
}
}
// 闭包 = 函数 + 外层变量的引用
func main() {
// 传入 suffix 变量为 .txt
r := makeSuffixFunc(".txt")
// 返回的匿名函数
ret := r("哈哈哈")
fmt.Println(ret)
}
例二:
// 1. calc 为计算函数,接收一个参数 base
// 2. 返回值有两个,都是函数类型
// 3. 内部定义了 add 和 sub 两个函数,最后return
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
// 判断返回是不是闭包
// 就是判断返回函数的内部是否包含外层变量的引用
// 这里两个函数的 base 是外层变量引用,所以是闭包
}
func main() {
// 闭包 = 函数 + 外层变量的引用
// 100 就是外层的 base,100是内层的 i
x, y := calc(100)
ret1 := x(200) // base = 100 + 200
fmt.Println(ret1)
ret2 := y(200) // base = 300 - 200
fmt.Println(ret2)
}
4.5 延迟调用
当前函数执行结束前才被执行,常用于资源释放、锁定解除以及错误处理等。
func main() {
x, y := 1, 2
defer func(a int) {
fmt.Println("defer x , y = ", a, y) // y 是闭包
}(x) // 注册时复制参数,为1
x += 100
y += 200
fmt.Println(x, y)
}
// 101 202
// defer x , y = 1 202 // 延迟调用,最后执行
多个延迟注册按照FILO次序执行(先进后出)
性能要求高且压力大的算法,应该避免使用延迟调用
4.6 错误处理
官方推荐的做法是返回error状态
func Scanlan(a ...interface{})(n int, err error)
var errDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, errDivByZero
}
return x / y, nil
}
func main() {
z, err := div(5, 0)
if err == errDivByZero {
log.Fatalln(err) // 输出 2019/09/24 20:14:49 division by zero
}
fmt.Println(z)
}
panic,recover
和error相比,panic/recover在使用上更接近 try/catch 结构化异常
https://www.bilibili.com/video/av54285508
4.6 内置函数
内置函数 | 介绍 |
---|---|
close | 用来关闭channel |
len | 求长度,string、array、slice、map、channel |
new | 分配内存,主要用来分配值类型,int、struct,返回的是指针 |
make | 分配内存,主要用来分配引用类型 chan、map、slice |
append | 追加元素到数组、slice中 |
panic和recover | 错误处理 |