go语言学习(第四章,函数)(go 语言学习笔记)

4.1 定义

函数是结构化编程的最小模块单元。它将复杂的算法过程分解成若干较小任务,隐藏相关细节,使程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。
关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点。

  • 无需前置声明
  • 不支持命名嵌套定义
  • 不支持同名函数重载
  • 不支持默认参数
  • 支持不定长变参
  • 支持多返回值
  • 支持命名返回值
  • 支持匿名函数和闭包
package main

func data() []int 		//missing function body
{		//syntax error: unexpected semicolon or newline before
	return []int{1, 2, 3, 4, 5}
}
func data() []int {		//data redeclared in this block
	return []int{1, 2, 3, 4, 5}
}
func main() {
	func test() []int {		//syntax error: unexpected test, expecting (
	return []int{1, 2, 3, 4, 5}
}
}

函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。

package main

func hello() {
	println("a")
}
func exec(f func()) {
	f()
}

func main() {
	f := hello
	exec(f)
}

第一类对象指可在运行期间创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数。
从阅读和代码维护的角度来说,使用命名类型更加方便。

type FormatFunc func(string,...interface{})(string,error)

// 如果不使用命名类型,这个参数的签名就会长到没法看
func format(f FormatFunc,s string, a ...interface{})(string,error){
	return f(s,a...)
}

函数只能判断其是否为nil,不支持其他比较操作

package main

func a() {}
func b() {}
func main() {
	println(a == nil)
	println(a == b)	//invalid operation: a == b (func can only be compared to nil)
}

从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存

package main

func test()*int{
	a:=0x100
	return &a
}
func main() {
	var b *int = test()
	println(b,&b,*b)

}

4.2 参数

Go对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。调用时,必须按签名顺序传递指定类型参数,就算以“_”命名的参数也不能忽略
在参数列表中,相邻的同类型参数可合并

package main

func test(x, y int, s string, _ bool) *int {
	return nil
}
func main() {
	test(1, 2, "a") //not enough arguments in call to test

}

参数可视作函数局部变量,因此不能在相同层次定义同名变量

package main

func add(x, y int) int {
	x := 100  //no new variables on left side of :=
	var y int //y redeclared in this block
	return x + y
}
func main() {
	add(10, 20)

}

形参是指函数中定义的参数,实参则是函数调用时传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数。
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。

package main

import "fmt"

func test(x *int) {
	fmt.Printf("pointer:%p,target:%v\n", &x, x)
}
func main() {
	a := 0x100
	p := &a
	fmt.Printf("pointer:%p,target:%v\n", &p, p)
	test(p)
}

从表面上看,指针参数的性能更好一些,但实际上得具体分析。被复制的指针会延长目标对象声明周期,还可能导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
其实,在栈上复制小对象只需很少的指令即可完成,远比运行时进行堆内存分配要快得多,另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态
要实现传出参数,通常建议使用返回值,当然也可继续使用二级指针

package main

func test(p **int) {
	x := 100
	*p = &x
}
func main() {
	var p *int
	test(&p)
	println(*p)		//100
}

如果函数参数过多,建议将其重构为一个复合结构类型,也算是变相实现可选参数和命名实参功能。

package main

import (
	"fmt"
	"log"
	"time"
)

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/teset",
		timeout: time.Second * 5,
		log:     nil,
	}
}
func server(option *serverOption) {
	(*option).address = "127.0.0.1"
	option.port = 8000
	fmt.Printf("%v\n", option)
}
func main() {
	opt := newOption()
	server(opt)
}
结果
&{127.0.0.1 8000 /var/teset 5000000000 <nil>}

将过多的参数独立成option struct ,即方便扩展参数集,也方便通过newOption函数设置默认配置。这也是代码复用的一种方式,避免多处调用时烦琐的参数配置

变参

变参本质上就是一个切片。只能接收一个或多个同类型参数,且必须放在列表末尾。

package main

import (
	"fmt"
)

func test(s string, a ...int) {
	fmt.Printf("%T,%v\n", a, a)
}
func main() {
	test("a", 1, 2, 3, 4)
}
[]int,[1 2 3 4]

将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。

package main

import (
	"fmt"
)

func test(s string, a ...int) {
	fmt.Printf("%T,%v\n", a, a)
}
func main() {

	s := [4]int{4, 5, 6, 7}
	test("a", s[:]...)
}

既然变参是切片,那么复制的仅是切片自身,并不包含底层数组,也因此可以修改原数据,如果需要,可用内置函数copy复制底层数据

package main

import "fmt"

func test(a ...int) {
	for i := range a {
		a[i] += 100
	}
}
func main() {

	s := [4]int{4, 5, 6, 7}
	test(s[:]...)
	fmt.Println(s)
}

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 {
		break
	}
} //missing return at end of function

借鉴自动态语言的多返回值模式,函数得以返回更多状态,尤其是error模式。

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errors.New("division by zero")
	}
	return x / y, nil
}

稍有不便的是没有元组类型,也不能用数组,切片接收,但可用“_”忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当做结果直接返回

命名返回值

对返回值命名和简短变量定义一样,优缺点共存

func paging(sql string, index int) (count int, pages int, err error) {
	return
}

命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示。
命名返回值和参数一样,可当做函数局部变量使用,最后由return 隐式返回

func div(x, y int) (z int, err error) {
	if y == 0 {
		err = errors.New("division by zero")
		return
	}
	z = x / y
	return
}

这些特殊的“局部变量”会被不同层级的同名变量遮蔽。好在编译器能检查到此类情况,只要改成显示return返回即可

func add(x, y int) (z int) {
	{
		z := x + y
		return // z is shadowed during return
	}
	return
}

除遮蔽外,我们还必须对全部返回值命名,否则编译器会搞不清状况

func test() (int, s string, e error) {
	return 0, "", nil //cannot use 0 (type int) as type string in return argument
}

显然编译器在处理return语句时,会跳过未命名返回值,无法准确匹配。
如果返回值类型能明确表明其含义,就尽量不要对其命名

4.4 匿名函数

匿名函数是指没有定义名字符号的函数。
除没有名字外,匿名函数和普通函数完全相同。最大区别是,我们可以在函数内定义匿名函数,形成类似嵌套效果。匿名函数可以直接调用,保存到变量,作为参数或返回值。
直接执行:

func(s string){
		println(s)
	}("hello")

赋值给变量

func main() {
	add := func(x, y int) int {
		return x + y
	}
	println(add(1, 2))

}

作为参数:

func test(f func()) {
	f()
}

func main() {
	test(func() { println("hello") })
}

作为返回值

func test() func(int, int) int {
	return func(x, y int) int {
		return x + y
	}
}

func main() {
	add := test()
	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(1,2))
}
func testChannel(){
	c := make(chan func(int,int) int,2)
	c <- func(x,y int)int{
		return x + y
	}
	println((<-c)(1,2))
}

不曾使用的匿名函数会被编译器当做错误

func main() {
	func(x int) int { //func literal evaluated but not used
		return x
	}
}

除闭包因素外,匿名函数也是一种常见重构方法。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离开(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码结构。

闭包

闭包是在其词法上下文中引入了自由变量的函数,或者说是函数和其引用的环境的组合体。

package main

func test(x int) func(){
	return func(){
		println(x)
	}
}
func main() {
	f := test(22)
	f()
}

就这段代码而言,test返回的匿名函数会引用上下文变量x。当该函数在main中执行时,它依然可以正确读取x的值,这种现象就称为闭包。


func test(x int) func(){
	println(&x)
	return func(){
		println(&x,x)
	}
}
func main() {
	f := test(22)
	f()
}
结果
0xc000040000
0xc000040000 22

通过输出指针,我们注意到闭包直接引用了环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓延迟求值的特性

package main

func test()[]func(){
	var s []func()
	for i:=0;i<2;i++{
		s = append(s,func(){
			println(&i,i)
		})
	}
	return s
}
func main() {
	for _,i := range test(){
		i()
	}
}
结果:
0xc000040000 2
0xc000040000 2

对于这个输出结果不必惊讶,很简单,for循环复用局部变量i,那么,每次添加的匿名函数引用的自然是同一变量。添加操作仅仅将匿名函数放入列表,并未执行。因此,当main执行这些函数时,它们读取的是环境变量最后一次循环时的值。
解决方法就是每次用不同的环境变量或传参复制,让各自闭包环境不同

package main

func test()[]func(){
	var s []func()
	for i:=0;i<2;i++{
		x := i
		s = append(s,func(){
			println(&x,x)
		})
	}
	return s
}
func main() {
	for _,i := range test(){
		i()
	}
}
结果
0xc000040000 0
0xc000040008 1

多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何修改行为都会影响其他函数取值,在并发模式下可能要做同步处理

func test(x int)(func(),func()){
	return func(){
		println(x)
		x += 100
	},func(){
		println(x)
	}
}
func main() {
	func1,func2:= test(22)
	func1()
	func2()
}
结果
22
122

闭包让我们不用传递参数就可以读取或修改环境状态,当然也要为此付出额外代价。对于性能要求比较高的场合,须慎重使用

4.5 延迟调用

语句defer向当前函数注册稍后执行的函数调用。这些调用被称为延迟调用,因为他们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	f, err := os.Open("./5.go")
	if err != nil {
		log.Fatalln(err)
	}
	defer f.Close()
	fmt.Printf("%T", f)
}

注意延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并保存起来。如果状态敏感,可改用指针或闭包

package main

func main() {
	x, y := 1, 2
	defer func(a int) {
		println("defer a,x,y", a, x, y)
	}(x)
	x += 100
	y += 200
	println(x, y )
}
result:
101 202
defer a,x,y 1 101 202

多个延迟注册按FILO次序执行

package main

func main() {
	defer println("x")
	defer println("y")
}
result:
y
x

编译器通过插入额外的指令来实现延迟调用执行,而return和panic语句都会终止当前函数流程,引发延迟调用。另外,return不是ret汇编语句,他会先更新返回值

package main

func test()(z int){
	defer func(){
		println("defer z:",z)
		z += 100
	}()
	return 100
}
func main() {
	println("test:",test())
}
result:
defer z: 100
test: 200
误用

延迟调用在调用函数结束时才会被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
案例:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长。

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	for i := 0; i < 10000; i++ {
		path := fmt.Sprintf("./log/%d.txt", i)
		f, err := os.Open(path)
		if err != nil {
			log.Println(err)
			continue
		}
		defer f.Close()
	}
}

应该直接调用,或重构成为函数,将循环和处理算法分离

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	do := func(x int){
		path := fmt.Sprintf("./log/%d.txt",x)
		f,err := os.Open(path)
		if err != nil{
			log.Println(err)
		}
		defer f.Close()
	}
	for i:=0;i<1000;i++{
		do(i)
	}
}

性能

相比直接调用CALL汇编指令调用函数,延迟调用则须花费更大的代价,这其中包括注册调用等操作,还有额外的缓存开销

4.6错误处理

反古的错误处理方式,是Go被谈及最多的内容之一,有人戏称“struck in
70‘s”,可见它与流行趋势背道而驰。

error

官方推荐的标准做法是返回error状态。

func Scanla(a ...interface{})(n int,err error)

标准库将error定义为接口类型,以便实现自定义错误类型。

type error interface {
	Error() string
}

按惯例,error总是最后一个返回参数。标准库提供了相关创建函数,可方便地创建包含简单错误文本的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)
	}
	println(z)
}

应通过错误变量,而文本内容来判定错误类型
错误变量通常以err作为前缀,且字符串内容全部小写,没有结束标点,以便嵌入到其他格式化字符串中输出。
全局错误变量并非没有问题因为他们可能被用户重新赋值,这就可能导致结果不匹配。
与errors.New类似的还有fmt.Errorf,它返回一个格式化内容的错误对象
某些时候,我们需要自定义错误类型,以容纳更多的上下文状态信息。这样的话,还可基于类型做出判断。

package main

import (
	"fmt"
	"log"
)

type DivError struct {
	x, y int
}

func (DivError) Error() string {
	return "division by zero"
}
func div(x, y int) (int, error) {
	if y == 0 {
		return 0, DivError{x, y}
	}
	return x / y, nil
}

func main() {
	z, err := div(5, 0)
	if err != nil {
		fmt.Printf("%T,%d\n", err, err)
		switch e := err.(type) {
		case DivError:
			fmt.Println(e, e.x, e.y)
		default:
			fmt.Println(e)
		}
		log.Fatalln(err)
	}
	println(z)
}
结果
main.DivError,{5 0}
division by zero 5 0
2019/03/04 21:03:55 division by zero
exit status 1

大量函数和方法返回error,使得调用代码变得很难看,一堆堆得检查语句充斥在代码行间。解决思路有:

  • 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码。
  • 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)。
  • 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
panic,recover

与error相比,panic/recover在使用上更接近try/catch结构化异常

func panic(v interface{})
func recover() interface()

比较有趣的是,它是内置函数而非语句。panic会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。

func main() {
	defer func() {
		if err := recover(); err != nil {
			log.Fatalln(err)
		}
	}()
	panic("i am dead")
	println("ok")
}

因为panic参数是空接口类型,因此可使用任何对象作为错误状态。而recover返回结果同样要做转型才能获得具体信息。
无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿着调用栈堆向外传递,要么被外捕获,要么导致程序崩溃。

func test() {
	defer println("test.1")
	defer println("test.2")
	panic("i am out")

}
func main() {
	defer func() {
		log.Println(recover())
	}()
	test()
}
结果:
test.2
test.1
2019/03/04 21:34:40 i am out

连续调用panic,仅最后一个会被recover捕获

package main

import "log"

func main() {
	defer func() {
		for {
			if err := recover(); err != nil {
				log.Println(err)
			} else {
				log.Fatalln("fatal")
			}
		}
	}()
	defer func() {
		panic("defer panic")
	}()
	panic("main panic")
}
结果
2019/03/04 21:38:13 defer panic
2019/03/04 21:38:13 fatal
exit status 1

在延迟函数中panic,不会影响后续延迟调用执行。而recover之后panic,可被再次捕获。另外,recover必须在延迟调用函数中执行才能正常工作

func catch() {
	log.Println("catch:", recover())
}
func main() {
	defer catch()		// 它捕获到了
	defer log.Println(recover())
	defer recover()

	panic("out ...")
}
结果
2019/03/04 21:42:40 <nil>
2019/03/04 21:42:40 catch: out ...

考虑到recover特性,如果要保护代码片段,那么只能将其重构为函数调用

package main

func test(x, y int) {
	z := 1
	func() {
		defer func() {
			if recover() != nil {
				println("????")
				z = 0
			}
		}()
		z = x / y
	}()
	println("x/y=", z)
}
func main() {
	test(5, 0)
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值