首页 > 技术文章 > 一天搞懂Go语言(3)——函数和方法

yrxing 2020-12-21 14:54 原文

函数

函数声明

  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 io
import "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:=square
f(3)

f=product //错误,类型不一致

var f func(int) int //定义一个空值函数变量
f(3) //宕机:调用空函数

//函数变量只能和nil比较

 匿名函数

  匿名函数的作用类似于java里面的匿名函数,通常一些短小不常用的函数嵌入到调用者形参里。匿名函数func关键字后面没有函数名,他的值是一个函数变量。

func squares() func() int{
    var x int
    return 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.Mutex
var 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
}

//增加defer
func 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*=factor
    p.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{
    Point
    Color 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()。

 

推荐阅读