目录
1.函数
如下函数使用func标识,包含函数签名和函数体。
func name(parameter-list) (result-list) {
body
}
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
1.1 函数声明
函数包含签名和函数体,函数体很好理解,就是语句的集合
签名包含func、函数名、参数列表、返回值,与其它语言类似,比较明显的区别有两个
- 参数列表的参数名、参数类型的声明顺序,且相同类型可以简写
- 返回值是个列表,长度是[0,n],可以没有返回值(类似java void),也可有多个返回值;可以命名如下z
- 函数传参是值传递,传入拷贝的值,只有指针, slice, map, function, channel这5个引用类型修改在函数外有效,其余都无效
func add(x int, y int) int { return x + y }
func sub(x, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
1.1.1 第三方类库
函数就是为了复用,go团队在golang.org/x/仓库中提供了大量的非标准包,为什么不将它们放在标准库中呢?有的因为还在开发阶段,有的因为只有少部分人需要引用。
go get等获取golang.org/x/下的包一般会报错,因为外网防火墙的原因,所以一般从对应的github仓库将其下载到本地${GOPATH}/src/golang.org/x下,类似于maven的远程仓库和本地仓库,如下命令
# golang.org
git clone git@github.com:golang/net.git ${GOPATH}/src/golang.org/x/net
1.2 函数调用&递归
与大多数语言语法类似,如下,html是引入的包,Parse是export导出的方法,参数是标准输入,返回值赋值给doc和err。
doc, err := html.Parse(os.Stdin)
与大多数语言类似,支持递归,递归就是直接或者间接地调用自身,使用场景如树的深度遍历等等。
与大多数语言递归不同的时,很多语言如Java有栈大小(64KB-2MB)的限制,递归深度过大可能抛出栈溢出的异常,(通常是因为递归边界没有写好),但是Go的栈大小是可以动态增长地,一直到GB级别的大小。所以在递归时要更加注意边界,否则可能侵占更多资源,更长时间才能发现!!
1.3 返回值
Go可以有多个返回值,约定俗成的是一般函数调用返回结构和是否成功的标识符。如果不是以上的惯例需要通过函数命名传达返回值有几个、什么含义。
return result, nil // return语句
resp, err := http.Get(url) // 调用,返回值赋值
links, _ := findLinks(url) // errors ignored ,如果不需要某个返回值,需要将其赋值给_
1.4.1 命名返回值
如果返回值有了名字,则可以省略return语句后面的变量(裸return),因为其作为局部变量在函数被使用了。
func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
err = fmt.Errorf("parsing HTML: %s", err)
return
}
words, images = countWordsAndImages(doc)
return
}
但由于可读性不高,有歧义,最好谨慎使用裸return。
1.4 错误机制errors
函数分为两种,一种是确保能够正常执行,还有一种是不能确保能够正常执行(如读取问题,网络传输等等)。Go的异常处理(Error Handling),只有一种错误原因的像上文所述一样附带返回一个bool值表达是否成功即可,但大多数情况是有很多种失败的可能性,需要传递给用户错误码和原因,所以go内置了error类型(是个interface)如下,nil时表示功能,非nil时可以从中获取失败原因。
type error interface {
Error() string
}
1.4.1 传递错误
与其它语言的异常处理(exception mechanism)不同,没有throw 和catch等流程控制的机器,异常处理还是通过if和return等普通指令来完成,如下,异常处理就是判断错误err,如果有错误将继续往上抛(传递),如果一直抛到main函数还不处理的话会处理异常退出。
resp, err := http.Get(url)
if err != nil {
return nil, err // 原样抛出
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) // 处理后抛出,包装异常error msg
}
错误信息规范
error message不应该包含大写和换行符,这样使用grep进行搜索可以看到完整的错误链。
- 错误信息(error message)尽量详尽,同包风格保持一致
- 函数调用 函数体应传递函数、参数等上下文,调用处应补充信息
1.4.2 重试策略
如果错误是偶发,可以重试(限制重试的时间间隔或重试的次数)
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline := time.Now().Add(timeout)
for tries := 0; time.Now().Before(deadline); tries++ { // 时间限制
_, err := http.Head(url)
if err == nil {
return nil // 成功返回
}
log.Printf("server not responding (%s); retrying...", err)
time.Sleep(time.Second << uint(tries)) // exponential back-off
}
return fmt.Errorf("server %s failed to respond after %s", url, timeout) // 传递错误
}
1.4.3 终止执行&logerror
类似于Java的RuntimeException,错误发生后,程序无法继续运行,则输出错误信息并结束程序。通常在main函数中进行,库函数应该往上抛出。
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) // 标准错误
os.Exit(1)
}
if err := WaitForServer(url); err != nil {
log.Fatalf("Site is down: %v\n", err) // 日志
}
1.4.4 不终止不处理
类似Java中的很多受检查的非RuntimeException,catch住后可以不进行强制处理,打印相关日志或者什么都不做,然后继续执行。
1.5 函数值!!
函数像其他值一样,
- 拥有类型, 如func(rune) rune,展示了参数和返回值列表
- 可赋值给变量,
- 传递给函数,
- 从函数返回
- 可以被调用
函数类型的零值是nil,调用值为nil的函数值会引起panic错误,但是两个非nil的函数值不能==比较,也就不能作为map的key。
函数值的出现,类似于C的函数引用,可以作为参数传递给另一个函数进行调用,这样函数调用不仅仅可以传递数据,也可以传递行为。
标准类库strings.Map,声明和调用如下,对字符串中的每个字符调用add1函数,并将每个add1函数的返回值组成一个新串返回。
func Map(mapping func(rune) rune, s string) string {} // 第一个mapping函数,将第二个参数string每个字符进行映射
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"
这种机制可以促进抽象,类似于模板模式,将不同的行为作为参数传入,相同的流程步骤放大模板方法,这样的设计更抽象、灵活。
1.6 匿名函数
拥有函数名的函数只能在包级语法块中被声明,为了绕过这一限制,type或者func内部可以使用函数字面量(匿名函数),类似于Java中的匿名类,如果不需要复用或者缩小其scope,可以使用匿名函数。于函数声明的区别就是没有函数名。
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
1.6.1 闭包
匿名函数有一个很有用的特性,内部的匿名函数可以访问其enclosing函数(外部函数)的变量;类似于Java的内部类,也有个指向外部类的引用,匿名函数也有引用指向外部函数变量,函数值不仅仅是一串代码,还记录了状态。这也是为什么函数值属于引用类型,为什么函数值不可比较的原因。
func squares() func() int {
var x int
return func() int {
x++ // 闭包,可以访问其outer的变量
return x * x
}
}
当匿名函数需要递归的时候怎么办呢??
- 定义函数类型变量
- 将匿名函数赋值给变量
- 匿名函数内部 调用函数值变量
var visitAll func(items []string) // 函数值变量,不是函数名哦~
visitAll = func(items []string) { // 匿名函数 DFS的方式
for _, item := range items {
if !seen[item] { // 闭包访问
seen[item] = true
visitAll(m[item]) // 递归 最终将叶子先放入结果中
order = append(order, item)
}
}
}
需要注意的是,
- x已经不再是个单纯的局部变量,它的scope不一样,在外部函数squares return返回了以后,x还不会消失,因为f还引用这它。
- 匿名函数内部不要引用外部for的迭代变量,因为如下的匿名函数中引用的dir只记录了内存地址,所以是同一个,而不是每次迭代的值。
var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // NOTE: incorrect!
})
}
// 引入同名变量
for _, dir := range tempDirs() {
dir := dir // declares inner dir, initialized to outer dir
// ...
}
1.7 可变参数
函数可以接收任意个数的参数,Java也有类似的机制,如Java的main函数,可以接收任意个程序参数。如下表示接收任意个int类型的参数。常用格式化输出函数就是典型,如下,interfac{}表示函数的最后一个参数可以接收任意类型。
func Printf(format string, a ...interface{}) (n int, err error) {}
func sum(vals...int) int {}
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10" 传入切片,后面加...
原理就是:隐式创建数组arr,拷贝原始参数,再把切片arr[:]作为参数传给被调函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。
1.8 Deferred函数调用
为了保证资源释放和去除冗余,在Java中我们会在try-catch-finally的finally块中进行资源释放,Go也有类似的机制,就是defer。
如果没有defer机制的话,在代码执行的过程中,异常和正常逻辑 块中都需要写资源释放的代码,这样就会有很多冗余,也会有遗漏的情况,维护也很困难。
语法简单,只需在调用普通函数或方法前加上关键字defer,执行过程是 当前函数执行完毕(return)后才会执行defer后接的函数。就像是try-catch包含了整个的函数代码,defer后面的函数就是finally块。
// IO资源
defer resp.Body.Close()
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]
}
常用于资源清退,也用于函数trace等。
注意,如果需要在循环体中多次调用defer语句,应抽取新的子函数,否则不会迭代调用,仅在函数退出时调用一次。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈,所以需要关注colse的异常,如果没有检查文件关闭时的error,可能会导致数据丢失,所以写入文件后关闭文件时,我们不对f.close采用defer机制。
1.9 Panic异常
java在遇到throw了RuntimeException时,会执行outer的finally块,然后线程终止,一般退出时会打印异常堆栈信息到标准错误。Go的panic机制也是类似的,panic异常发生,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制),程序崩溃并输出日志信息,日志信息包括panic value和函数调用的堆栈跟踪信息。
func main() {
f(1)
}
func f(x int) {
fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
defer fmt.Printf("defer %d\n", x)
f(x - 1)
}

内置panic函数类似于Java的throw,可以抛出一个运行时异常,引起程序的崩溃。尽量少用panic,尽量使用上文1.4的错误机制。
switch s := suit(drawCard()); s {
case "Spades": // ...
case "Hearts": // ...
case "Diamonds": // ...
case "Clubs": // ...
default:
panic(fmt.Sprintf("invalid suit %q", s)) // Joker? 类似于断言抛出运行时异常
}
1.10 捕获异常
既然有了类似于java的throw的panic,也得有类似于java的catch的recover来捕获异常。就像Java异常分为运行时异常的分类,程序中选择性地在执行过程中恢复或者终止。Go也是这样,只恢复应该被恢复的panic异常,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理。
defer func() {
switch p := recover(); p {
case nil:
// no panic
case bailout{}:
// "expected" panic
err = fmt.Errorf("multiple title elements")
default:
panic(p) // unexpected panic; carry on panicking
}
}()
本文详细介绍了Go语言中的函数,包括函数声明、调用、返回值、命名返回值、错误处理机制、函数值、匿名函数和闭包、可变参数、Deferred函数调用、 Panic异常及捕获异常。重点讨论了Go的错误处理方式,强调了动态增长的栈对于递归的影响,以及错误信息的规范化。此外,还提到了函数值作为行为传递的灵活性和闭包的特性。

被折叠的 条评论
为什么被折叠?



