分享
  1. 首页
  2. 文章

golang的函数与方法

123archu · · 1344 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

函数

//常规的函数定义
func 方法名(参数列表) 返回值 {
 定义
}

函数的值(闭包)

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误

函数值不仅仅是一串代码,还记录了状态。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。我们看个闭包的例子:

func f1(limit int) (func(v int) bool) {
 //编译器发现limit逃逸了,自动在堆上分配
 return func (limit int) bool { return v>limit} 
}
func main() {
 closure := f1(5)
 fmt.Printf("%v\n", closure(1)) //false
 fmt.Printf("%v\n", closure(5)) //false
 fmt.Printf("%v\n", closure(10)) //true
}

在这个例子中,f1函数传入limit参数,返回一个闭包,闭包接受一个参数v,判断v是否大于之前设置进去的limit。

可变参数列表

可变参数,即参数不是固定的,例如fmt.Printf函数那样,注意只有最后一个参数才可以是声明为可变参数,声明:

func 函数名(变量名...类型) 返回值

我们看个例子:

package main
import (
 "fmt"
)
func f1(name string, vals... int) (sum int) {
 for _, v := range vals {
 sum += v
 }
 sum += len(name)
 return
}
func main() {
 fmt.Printf("%d\n", f1("abc", 1,2,3,4 )) //13
}

函数的延迟执行 defer

包含defer语句的函数执行完毕后(例如return、panic),释放堆栈前会调用被声明defer的语句,常用于释放资源、记录函数执行耗时等,有一下几个特点:

  1. 当defer被声明时,其参数就会被实时解析
  2. 执行顺序和声明顺序相反
  3. defer可以读取有名返回值

看个例子:

package main
import (
 "fmt"
)
//演示defer的函数可以访问返回值
func f2() (v int) {
 defer func (){ v++}()
 return 1 //执行这个时,把v置为1
}
//演示defer声明即解释
func f3(i int) (v int) {
 defer func(j int) { v += j} (i) //此时函数i已被解析为10,后面修改i的值无影响
 v = i
 i = i*2
 return
}
//演示defer的执行顺序,与声明顺序相反
func f4() {
 defer func() {fmt.Printf("first\n")} ()
 defer func() {fmt.Printf("second\n")} ()
}
func main() {
 fmt.Printf("%d\n", f2()) // 13
 fmt.Printf("%d\n", f3(10)) // 20
 f4() //second\nfirst\n
}

典型的使用场景,函数执行完毕关闭资源:

func do() error {
 f, err := os.Open("book.txt")
 if err != nil {
 return err
 }
 defer func(f io.Closer) {
 if err := f.Close(); err != nil {
 // log etc
 }
 }(f)
 // ..code...
 f, err = os.Open("another-book.txt")
 if err != nil {
 return err
 }
 defer func(f io.Closer) {
 if err := f.Close(); err != nil {
 // log etc
 }
 }(f)
 return nil
}

在这里例子中可以看到,我们判断了Close()是否成功,因为在一些文件系统中,尤其是NFS,写文件出错往往被延迟到Close的时候才反馈,所以必须检查Close的状态。

异常panic

Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制,但这些机制仅仅用于严重的错误,而不是那些在健壮程序中应该被避免的程序错误。runtime在一些情况下会抛出异常,例如除0,我们也能使用panic关键字自己抛出异常

panic(异常的值) //值是啥都行

出现异常之后,默认情况就是程序退出并打印堆栈:

package main
func f6() {
 func () {
 func () int {
 x := 0
 y := 5/x
 return y
 }()
 }()
}
func main() {
 f6()
}

输出

panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.f6.func1.1(...)
 /Users/kitmanzheng/study/go/src/test_func.go:8
main.f6.func1()
 /Users/kitmanzheng/study/go/src/test_func.go:10 +0x11
main.f6()
 /Users/kitmanzheng/study/go/src/test_func.go:11 +0x20
main.main()
 /Users/kitmanzheng/study/go/src/test_func.go:16 +0x20
exit status 2

如果不想程序退出的话,也有办法,就是使用recover捕捉异常,然后返回error。在没发生panic的情况下,调用recover会返回nil,发生了panic,那么就是panic的值。看个例子:

package main
import (
 "fmt"
)
type shouldRecover struct{}
type emptyStruct struct{}
func f6() (err error) {
 defer func () {
 switch p := recover(); p {
 case nil: //donoting
 case shouldRecover{}:
 err = fmt.Errorf("occur panic but had recovered")
 default:
 panic(p)
 }
 } ()
 func () {
 func () int {
 panic(shouldRecover{})
 //panic(emptyStruct{})
 x := 0
 y := 5/x
 return y
 }()
 }()
 return
}
func main() {
 err := f6()
 if err != nil {
 fmt.Printf("fail %v\n", err)
 } else {
 fmt.Printf("success\n")
 }
}

输出
fail occur panic but had recovered
在这个例子中,我们手动抛出一个panic,值是shouldRecover,然后外层使用defer + 匿名函数 + recover去捕捉异常,发现panic的值是shouldRecover那么就不退出,而是返回error。

方法

//这种只能给type定义的类型用 
func (type类型参数) 方法名(参数列表) 返回值 {
 定义
}
//eg:
func (t TestType) testFunc() int {
 //...
}

例子中t称为接收器,可以是该类型本身,或该类型的指针,由于是值传递,所以是接收器是该类型时,会复制值,类型比较大时开销大,可以选择使用指针降低开销。而且在使用defer的时候,由于值复制,如果不用指针,变量发生了变化,但是defer运行时还是基于老变量运行的,容易会造成一些坑,除非你明确知道自己要这么做。建议func (*type)而不是func(type)。但是如果一个类型低层实际是一个指针,那么不允许在使用该类型的指针作为接收器。

当我们使用指针作为接收器时,记得检查是否是nil

看下面这个例子:

type myInt struct {
 owner string
 value int
}
func (a myInt) Owner(suffix string) string { //golang不支持默认参数
 return a.owner + suffix
}
func (a *myInt) SetOwner(owner string) {
 if a == nil {
 fmt.Println("set owner to nil point is invalid")
 return
 }
 a.owner = owner
}
func (a myInt) SetOwner2(owner string) { //golang函数参数按值传递,所以这个方法实际只是修改临时变量的owner
 a.owner = owner
}
func SetOwner3(a *myInt, owner string) {
 if a == nil {
 fmt.Println("set owner to nil point is invalid")
 return
 }
 a.owner = owner
}
func main() {
 var k = myInt{"kitman", 3}
 fmt.Print(k.value, " ", k.Owner("aa"), "\n") //输出3 kitmanaa
 k.SetOwner("ak") //相当于SetOwner(&k, "ak")
 fmt.Print(k.value, " ", k.Owner("bb"), "\n") //输出3 akbb
 k.SetOwner2("sss") //相当于SetOwner(k, "sss")
 fmt.Print(k.value, " ", k.Owner("bb"), "\n") //输出3 akbb
 SetOwner3(&k, "sss")
 fmt.Print(k.value, " ", k.Owner("bb"), "\n") //输出3 sssbb
 var k2 *myInt = nil
 k2.SetOwner("aa") //输出set owner to nil point is invalid
}

输出
3 kitmanaa
3 akbb
3 akbb
3 sssbb
set owner to nil point is invalid
通过上面的例子,我们可以发现一些知识点:

  1. 使用第二种函数定义的方法,那么就和c++的类差不多。本质上和普通函数一样,就是语法上的差别而已。
  2. 就算给type类型定义方法,函数参数也是按值传递的,所以type参数使用指针才能修改变量。
  3. nil指针也能调用方法,但是如果方法里面没判断指针是否是nil,那么就会core

面向对象继承语义

可以通过使用匿名成员 + 定义方法,实现部分继承的语义:

package main
import (
 "fmt"
)
type Base struct {
 y int
 Y int
}
func (b *Base) FuncByPoint() int {
 if (b == nil) {
 return 0;
 }
 return b.y*b.Y
}
func (b Base) FuncByValue() int {
 return b.y*b.Y
}
type Child struct {
 Base
 x int
 X int
}
func (c *Child) FuncByPoint() int {
 if (c == nil) {
 return 0
 }
 return c.x*c.X
}
func main() {
 var c Child
 c.y = 2
 c.Y = 3
 fmt.Printf("%v\n", c.FuncByPoint()) //0
 fmt.Printf("%v\n", c.Base.FuncByPoint())//6
 fmt.Printf("%v\n", c.FuncByValue()). //6
 var f1 func() int
 f1 = c.FuncByPoint
 fmt.Printf("%v\n", f1()) //0
 var f2 func(*Child) int
 f2 = (*Child).FuncByPoint
 fmt.Printf("%v\n", f2(&c)) //0
}

这个例子可以看到,Base中定义的方法,被外层的同名方法覆盖,需要显式指明才能调用到Base中的方法。注意golang中不存在真正的继承,这是嵌入匿名成员,用匿名成员的方法去理解这样的语法。另外,方法的值也是第一类变量,能赋值给别的变量,比c/c++灵活,golang无论是对象方法,还是类型的方法,都能赋值给别的变量,可以参照例子中的写法。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:123archu

查看原文:golang的函数与方法

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1344 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏