函数

函数签名:函数参数列表 + 函数返回值

  • func(i int, s string) string
  • 签名为:func(int, string) string

多返回值

标准库中的函数通常返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息。

如果一个函数将所有返回值都进行显示的命名,那么该函数的return语句可以省略操作数,即 bare return(裸返回)

错误

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。

  • 如果导致失败的原因只有一个,则额外的返回值可以为 bool 值,通常命名为ok
  • 通常,导致失败的原因不止一种,尤其对io操作而言,用户需要了解更多错误信息。因此,额外的返回值为 error 类型

error类型

内置的error类型是接口类型

  • error类型的值可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败

  • 当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值如果有用,我们应优先处理这些不完整的数据,再处理错误。(比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息)

  • 因此对函数的返回值要有清晰的说明,以便于其他人使用。

错误处理策略

传递错误

传播错误是最常用的错误处理策略。

  • 这意味着函数中某个子程序的失败,会变成该函数的失败

函数示例:

  • Case1: 如果findLinks对http.Get调用失败,findLinks会直接将这个HTTP错误返回给调用者
1
2
3
4
5
6
7
8
func findLinks(url string) ([]string, error) {
    resp, err := http.Get(url)
    if err != nil{
        return nill, err    // return error
    }
    defer resp.Body.Close()
    /* some other logic*/
}
  • Case2: 当对html.Parse的调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要信息:1、错误发生在解析器;2、url已经被解析。这些信息有助于错误的处理,findLinks会构造新的错误信息返回给调用者:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func findLinks(url string) ([]string, error) {
    resp, err := http.Get(url)
    if err != nil{
        return nill, err    
    }
    defer resp.Body.Close()
    
    doc, err := html.Parse(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) // return self-define error
    }
    /* some other logic*/
}

Note:

  • fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回
  • 使用该函数前缀添加额外的上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链。
  • 错误信息中应避免大写和换行符
  • 编写错误信息时,要注意错误信息表达的一致性(即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的)
  • Case3: 以OS包为例,OS包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在),也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。

Note: 一般而言,被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。

重新尝试失败的操作

如果发生的错误是偶然的,或由不可预知的问题导致,我们需要重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制重试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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.Get(url)
		if err == nil {
			return nil // success
		}
		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)
}

输出错误并结束程序

当发生的错误导致程序无法继续运行,这时应采用第三种策略: 输出错误信息并结束程序

Note: 这种策略只应在 main 中执行;对库函数而言,应向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了 bug,才能在库函数中结束程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// (In function main.)
if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

// or execute log.Fatalf()
if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

输出错误不结束程序

有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。可通过 log 包的函数处理

1
2
3
4
5
6
7
8
if err := Ping(); err != nil {
    log.Printf("ping failed: %v; networking disabled", err)
}

// or output to os.stdout
if err := Ping(); err != nil {
    fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled", err)
}

Note: log 包中的所有函数会为没有换行符的字符串增加换行府

忽略错误

在某些情况下,虽然程序没有处理错误,但程序的逻辑不会因此受到影响时,我们可以忽略该错误。但需要记录忽略错误的意图。

1
2
3
4
5
6
7
8
// get temp dir
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
    return fmt.Errorf("failed to create temp dir: %v", err)
}

// use temp dir
os.RemoveAll(dir) // ignore errors: $TMPDIR is cleaned periodically

Go错误处理风格

  • 代码结构:

    • 初始检查(参数、逻辑、格式、数据有效性…)等,防止错误发生

    • 函数的实际逻辑

Note: Go程序中,当检查某个子函数是否失败后,通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应该放在else语句块中,而应直接放在函数体。

EOF错误

EOF: 文件结尾错误,io包保证任何文件结束引起的读取失败都返回同一个错误——io.EOF, 该错误在io包中定义:

1
2
3
4
5
6
package io

import "errors"

// EOF is the error returned by Read when no more input is avaliable.
var EOF = errors.New("EOF")

函数值

在Go中,函数为第一类值:函数像其他值一样,有类型,可被赋值给其他变量,传递给函数和从函数返回。

函数类型的零值是 nil。调用值为 nil 的函数值会引起 panic 错误。

函数值可以与 nil 比较,但函数值之间不可比较,也不能用函数值作为map的key

1
2
3
4
var f func(i int) int 
if f != nil {
    f(i)
}

可变参数函数

可变参数函数: 参数数量可变的函数, 在声明可变参数函数时,需要在参数列表的最后一个参数类型前面加上省略符号 “…”, 这表示函数将接收任意数量的该类型的参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func sum(vals ...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"

Note: 函数体中,vals被看作类型 []int 的切片

给可变参数函数传入一个切片类型的变量:即在最后一个参数后加上省略符 ...

1
2
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)

内建函数

defer函数

defer关键字:允许在任意位置执行 return 语句之后, 才执行某个函数或方法

defer使用场景:

  • 释放资源,或成对处理的操作(Open&Colse, Connect&Disconnect, Lock&Unlock)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Operate file
func ReadFile(filename string) ([]byte, error) {
    f, err = os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close 
    return ReadAll(f)
}

// Operate mutex
var mu sync.Mutex
var cache = make(map[string]int)
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return m[key]
}

// Connect DB
var dbUrl = "localhost:27017"
session, err := mgo.Dial(dbUrl)
if err != nil {
    fmt.Errorf("connect to %v failed. %v", dbUrl, err)
    return
}
defer session.Close()
  • 追踪代码执行情况
  • 被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
    return x + x
}

func triple(x int) (result int) {
    defer func() { result += x }()
    return double(x)
}
fmt.Println(triple(4)) // "12"

Note: 注意循环体中的 defer 调用,因为只有在函数执行完成后,这些被延迟的函数才会执行。

循环处理文件的一个例子:

1
2
3
4
5
6
7
for _, file := range files {
    f, err := os.Open(file)
    if err != nil {
        return err
    }
    defer f.Close() // Note: risky, could run out of file descriptors
}

Note

  • 上面代码回导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会关闭。
  • 针对这个问题的一种解决方案是:将循环体中的defer语句移植另外一个函数,然后再for…range中调用它,确保文件在处理完成后立即被关闭。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for _, file := range files {
    if err := doFile(file); err != nil {
        return err
    }
}

func doFile(file string) error {
    f, err := os.Open(file)
    if err != nil {
        return err
    }
    defer f.Close()
    // process f
}

Note: 当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)

1
2
3
4
5
6
func f() {
	for i := 0; i < 5; i++ {
		defer fmt.Printf("%d ", i)
	}
}
上面的代码将会输出4 3 2 1 0

new make函数

new 与 make

See Also

Thanks to the authors 🙂