函数
函数声明
go语言函数支持多返回值。
func name(parameter-list)(result-list){
body
}
当函数存在返回列表的时候,必须显示以return结束。(除了break,抛异常等操作)
func sub(x,y int) (z int){ z = x-y;return} //如果形参类型相同,可以写到一起;返回值也可以命名,这时候,每个命名的返回值会声明一个局部变量,并初始化零值,最后跟一个return即可func first(x int,_ int) int{return x} //空白标识强调这个形参在函数中未使用func zero(int,int) int{return 0}
形参和返回值名字不会影响函数签名。
Go语言没有默认参数值概念也不能指定实参名(和python不一样)。go语言是值传递,指针、slice、map、函数、通道是引用类型,使用时可能会间接改变实惨值。
如果函数的声明没有函数体,就说明这个函数使用了除了go以外的语言实现。
递归
许多编程语言使用固定长度的函数调用栈,大小在64KB到2MB之间。递归的深度会受限于固定长度的栈大小,所以当进行深度递归调用时必须提防栈溢出。相比于固定长度的栈,Go语言的实现使用了可变长度的栈,栈的大小会随着使用而增长,可达到1GB左右上限。
多返回值
题外话:Go语言的垃圾回收机制将会回收未使用的内存,但不能指望它会释放未使用的操作系统资源,比如打开的文件以及网络连接,必须显式的关闭它们。
调用者使用多返回值的函数时候,必须有变量承接返回值,如果不想使用,用\’_\’承接。
func f(a string)([]string,error){/*...*/return f(a);} //返回一个多值结果可以是调用另一个多值返回的函数//一个多值调用可以作为单独的实参传递给拥有多个形参的函数中log.Println(f(a))//等价于strings, err:=f(a)log.Println(strings,err)
错误
即使在高质量代码中,也不能保证一定能够成功返回,习惯上将错误值作为最后一个结果返回。如果错误只有一种情况,结果通常设置为布尔类型。
如果错误原因很多,那么错误类型往往是error,error是内置的接口类型。目前我们了解到,错误可能是空值(成功)和非空值(失败),通过调用fmt.Println(err)或fmt.Printf(\”%v\”,err)输出错误信息。与其他语言不一样,go语言通过使用普通的值而非异常来报告错误。
错误处理策略
首先最常用的是将错误传递下去:return nil,fmt.Errorf(\”parsing %s as HTML: %v\”,url,err)。错误返回信息要可读、有意义、格式一致。
第二种情况是对于不固定或者不可预测的错误,在短暂的间隔后对操作进行重试是合乎情理的,超出一定的重试次数和限定时间后再报错退出。
如果还不能顺利执行,调用者能够输出错误然后优雅的停止程序。
if err:=WaitForServer(url);err!=nil{ fmt.Fprintf(os.Stderr,\"Site is down:%v\\n\",err) os.Exit(1) //更加方便的调用 //log.Fatalf(\"Site is down:%v\\n\",err)}
第四,在一些错误情况下,只记录错误信息然后程序继续运行。
第五,在某些罕见的情况下我们可以直接安全地忽略掉整个日志。
go语言的错误处理有特定的规律,一般都是开头一连串的检查用来返回错误,检测到失败往往都是在成功之前。
文件结束标识
io包保证任何由文件结束引起的读取错误,始终都将会得到一个与众不同的错误——io.EOF。
package ioimport \"errors\"var EOF = errors.New(\"EOF\")//示例in:=bufio.NewReader(os.Stdin)for{r,_,err:=in.ReadRune()if err==io.EOF{break}if err != nil{return fmt.Errorf(\"read failed:%v\",err)}}
函数变量
类似于C语言中函数指针的概念。函数变量的类型就是函数的签名,它们可以赋给变量或者传递或者从其他函数中返回。用法基本和C语言中的函数指针一样。
func square(n int) int{return n*n}func product(man int) int{return m*n}f:=squaref(3)f=product //错误,类型不一致var f func(int) int //定义一个空值函数变量f(3) //宕机:调用空函数//函数变量只能和nil比较
&nbs3e41p;匿名函数
匿名函数的作用类似于java里面的匿名函数,通常一些短小不常用的函数嵌入到调用者形参里。匿名函数func关键字后面没有函数名,他的值是一个函数变量。
func squares() func() int{var x intreturn func()int{x++return x*x} //匿名函数可以使用外层的变量}
func main(){
f:=suares()
fmt.Println(f()) //1
fmt.Println(f()) //4
fmt.Println(f()) //9
}
函数变量类似于使用闭包方法实现的变量,Go程序员通常把函数变量称为闭包(闭包就是能够读取其他函数内部变量的函数,所以闭包可以理解成“定义在一个函数内部的函数“)。我们看到变量的生命周期不是由它的作用域决定的,由于在定义squares( )函数时指定了返回的类型是一个匿名函数,并且该匿名函数返回的类型是整型。所以在squares( )函数中定义了一个匿名函数,并且将整个匿名函数返回,匿名函数返回的是整型。在main( )函数中定义了一个变量f,该变量的类型是匿名函数,f( )表示调用执行匿名函数。最终执行完成后发现,实现了数字的累加。虽然squares()已经返回了,但是返回的值:func()还在全局变量中使用,三次调用 f(),因此返回值会保存在堆上,即使栈释放了内存资源,但func()保存在堆中,数据不会释放。
因为匿名函数(闭包),有一个很重要的特点:
它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。
如果匿名函数需要进行递归,必须先声明一个变量然后将你ing函数赋值给这个变量。如果将两个步骤合成一个生命,函数字面量将不能存在于函数变量的作用域中,这样也就不能递归调用自己了。
visitALL:=func(items []string){//...visitAll[m[item]] //compile error: undefined:visitAll//...}
警告:捕获迭代变量
这个是go语言词法作用域规则的陷阱,即使是有经验的程序员也会掉入这个陷阱。
var rmdirs []func()for _,d:=range tempDirs(){ dir:=d //这一行是必须的 os.MakeAll(dir,0755) rmdir = append(rmdir,func(){ os.RemoveAll(dir) })}for _,rmdir:=range rmdirs{ rmdir()}
为什么需要在循环体内将循环变量赋给一个新的局部变量dir,而不是直接使用?这个原因是循环变量的作用域的规则限制。在循环里创建的所有函数变量共享相同的变量——一个可访问的存储位置,而不是一个固定的值。dir变量的值在不断地迭代更新,因此当调用清理函数的时候,dir变量已经被每一次for循环更新多次,因此dir变量的实际值是最后一次迭代时的值。
//当需要存储迭代变量的时候,我们通常声明一个同名变量去饮用它for_,dir:=range tempDirs(){dir:=dir//...}
变长函数
在参数列表最后的类型名称之前使用省略号“…”表示声明一个变长函数,调用这个函数的时候可以传递该类型任意数目的参数。
func sum(val ...int)int{//...}sum(1,2,3,4)//实参是slice时候,在最后一个参数后面放一个省略号values:=[]int{1,2,3,4}sum(values...)
延迟函数调用
语法上,一个defer语句就是在普通函数或方法的调用之前加上defer关键字。无论是正常情况还是异常情况,实际的调用推迟到包含defer语句的函数结束后才执行,defer语句经常用于成对的操作,比如打开和关闭,加锁和解锁,即使是再复杂的控制流,资源在任何情况下都能够正确释放。正确使用defer语句的地方是在成功获得资源之后。
func ReadFile(filename string)([]byte,error){ f,err:=os.Open(filename) if err!=nil{ return nil,err } defer f.Close() return ReadAll(f)}//解锁一个互斥锁var mu sync.Mutexvar m=make(map[string]int)func lookup(key string) int{ mu.Lock() defer mu.Unlock() return m[key]}
延迟执行的函数在return语句之后执行,并且可以更新函数的结果变量。因为匿名函数可以捕获其外层函数作用域内的变量,所以延迟执行的匿名函数可以观察到函数的返回结果。
func double(x int)int{return x+ x}//增加deferfunc double(x int)(result int){defer func(){fmt.Printf(\"dounble(%d) = %d\\n\",result)}() //圆括号不要忘记return x+x}
宕机(panic函数)
出现运行时错误时会发生宕机,正常程序执行会终止,goroutine中的所有延迟函数会执行,然后程序会异常退出并留下一条日志消息。当碰到“不可能发生”的状况时,调用内置的宕机函数是最好的处理方式。
//只有发生严重错误时才会使用宕机,否则会毫无意义,只会带来多余的检查func Reset(x *Buffer){if x==nil{panic(\"x is nil\")}x.elements = nil}
当宕机发生时,所有延迟函数以倒序执行,从栈的最上面函数开始一直返回到main函数。
恢复(recover函数)
退出程序通常是正确处理宕机的方式,但也有例外,在一定情况是可以进行恢复的,至少有时候可以在退出前理清当前混乱的情况。如果内置的recover函数在延迟函数的内部调用,而且这个包含defer语句的函数发生宕机,revover会终止当前的宕机状态并且返回宕机的值。函数不会从之前宕机的地方继续运行而是正常返回。如果recover在其他任何情况下运行则它没有任何效果且返回nil。
func Parse(input string)(s *Syntax,err error){defer func(){if p:=recover();p!=nil{err = fmt.Error(\"internal error:%v\",p)}}()}
方法
这里方法的概念就是面向对象编程中的概念,go语言也支持面向对象编程,只不过没有class概念,取而代之的是用struct来完成面向对象。
方法声明
我们知道,方法是属于某一个类的,因此,在go语言中,只是在函数名字前面多一个参数,这个参数把这个方法绑定到参数对应的struct上。
type Point struct{x,y float64}//普通函数func Distance(p,q Point) float64{ return math.Hypot(q.x-p.x,q.y=p.y)}//包级别//Point类型方法func (p Point) Distance(q Point) float64{ return math.Hypot(q.x-p.x,q.y-p,y)}//这两个函数不会冲突,Distance相当于在struct结构里里面,和x,y在同一个作用域
附加参数p称为方法接受者,go语言中,接受者不适用特殊名字(this,self),而是我们自己选择。(最常用方法是取类型名称首字母)。
go和其他语言不一样,它可以将方法绑定到任何类型上。可以很方便的为简单类型定义附加行为。类型拥有的方法名必须是唯一的,但不同类型可以使用相同方法名。
指针接受者的方法
习惯上遵循如果Point的任何一个方法使用指针接受者,那么所有的Point方法都应该使用指针接受者。当实参接收者很大的时候,为了避免复制开销,通常将这种接受者定义为指针接收者。
func (p *Point) ScaleBy(factor float64){p.x*=factorp.y*=factor}//调用r:=&Point{1,3}r.ScaleBy(2)p:=Point{1,2}p.ScaleBy(2) //等价于(&)p.ScaleBy(2),编译器会隐式转换,只有变量才允许这么做。//不能够对一个不能取地址的接受者参数调用*Point方法,因为无法获取临时变量地址Point{1,2}.ScaleBy(2) //compile error
如果实参接受者是*Point类型,调用Point.Distance方法是合法的,因为我们有办法从地址中获取Point值。
//这两个调用效果一样,编译器也会隐式转换pptr.Distance(q)(*pptr).Distance(q)
nil是一个合法的接收者
就像一些函数允许nil指针作为实参,方法的接受者也一样。
通过结构体内嵌组成类型
type ColoredPoint struct{PointColor color.RGBA}
内嵌相当于把Point里面的东西都嵌入到ColoredPoint中,包括变量和方法,有点类似于继承的意思。因此我们可以定义ColoredPoint类型,使用Point方法。
//如果形参不一致,需要显式使用它var p:=ColoredPoint{***}var q:=ColoredPoint{***}p.Distance(q.Point) //显示使用
方法变量与表达式
通常调用方法都是使用p.Distance()形式,但是把这两个操作分开也是可以的。方法变量类似于函数变量,选择子p.Distance可以赋予一个方法变量,即把方法绑定到接收者上,函数只需要提供实参而不需要提供接受者就能够调用。
p:=Point{1,2}q:=Point{3,4}distanceFromP:=p.Distance //方法变量distanceFromP(q) //不需要接收者就可以调用
与方法变量相关的是方法表达式,把原来方法的接受者替换成函数的第一个形参,可以像调用平常函数一样调用方法。
distance:=Point.Distance //方法表达式distance(p,q)
如果需要用一个值来代表多个方法,方法变量可以帮助你调用这个值所对应的方法来处理不同的接受者。
封装
封装也被称为数据隐藏,go语言只有一种方式控制命名可见性:首字母的大小写。而在go语言中封装的单元是包而不是类型,也就是说,结构体内的字段对于同一个包都是可见的。在go语言的getter器命名时候通常将Get省略,比如Point类型中的X的getter和setter分别为X()和SetX()。