Go 语言编程 — 错误处理

举报
云物互联 发表于 2021/08/05 23:06:23 2021/08/05
【摘要】 目录 文章目录 目录Golang 的错误处理哲学为什么 Golang 不使用异常进行错误处理?错误处理错误处理策略建议 Golang 的错误处理哲学 首先需要注意的是,错误与异常有着本质的区别。 错误(Error):作为流程的一部分,被调用方显式返回,调用方主动处理。异常(Exception):预料之外出现或者在流程中不应该出现的错误。 Golan...

目录

Golang 的错误处理哲学

首先需要注意的是,错误与异常有着本质的区别。

  • 错误(Error):作为流程的一部分,被调用方显式返回,调用方主动处理。
  • 异常(Exception):预料之外出现或者在流程中不应该出现的错误。

Golang 的特征是不使用异常进行错误处理,这一点有别于 Python、JavaScript 等变成语言。

Golang 的错误处理哲学就是:将错误视为编写函数的一等公民(返回值)。认为错误是正常流程的一部分,应该直接返回(return)。

例如:

func getUserFromDB() (*User, error) { ... }

func main() { user, err := getUserFromDB()
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

在阅读代码时,开发者可以清晰的看到存在的错误。相较于其他语言,例如:Python,可能就无法这么清晰的来展现一个错误了,因为 try/catch 在处理控制流方面是完全不透明的。

以标准的方式处理 Golang 中的错误,将获得以下好处:

  1. 没有隐藏的控制流。
  2. 没有意外未捕获的异常日志。
  3. 可以完全控制代码中的错误,可以选择处理,返回和执行任何其他操作。

Golang 为程序员提供了对错误处理的完全控制权,但也要你承担全部的责任。所以 Golang 的错误处理一直存在着争议。

为什么 Golang 不使用异常进行错误处理?

Golang 之禅提到了两个重要的谚语:

  1. 简单很重要。
  2. 为失败计划而不是成功。

对所有返回 (value, error) 的函数使用简单的 if err!= nil 语句有助于确保首先考虑程序失败的情况。而无需费心处理复杂的嵌套 try/catch 块,它们可以适当地处理所有可能出现的异常。

对于基于异常的代码,你可能会意识到这样的一个场景:你会依赖 try/catch 的全局捕获,而不会深入挖掘实际的具体错误类型。这就导致了,代码有实际的异常,但你并没有正确处理它。也就是说,基于异常的代码鼓励开发者不检查错误,而是认为某些异常(如果发生)将在运行时自动处理。

但是退后一步说,基于异常的语言有一个好处:即使程序在运行时发生了未被处理的异常,仍可以通过堆栈来跟踪它,而不会中止程序。

错误处理

Golang 的错误(Error)是一个接口,不是某种特定类型。通过内置的 error interface 提供了非常简单的错误处理机制。func f() (value, error) 的语法不仅易于理解,而且在任何 Golang 项目中都可确保语法的一致性。

error interface 的定义:

type error interface { Error() string
}

  
 
  • 1
  • 2
  • 3

在代码中,可以通过实现 error 接口类型来生成错误信息。函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个自定义的错误信息:

func Sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("math: square root of negative number") } // 实现
}

result, err:= Sqrt(-1)

if err != nil { fmt.Println(err)
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

if err != nil 模式的优势在于,通过错误链能方便遍历程序层次结构,直到发生错误的地方。例如,由程序的 main 函数处理的常见 Go 错误可能如下所示:

[2020-07-05-9:00] ERROR: Could not create user: could not check if user already exists in DB: could not establish database connection: no internet

  
 
  • 1

上面的错误链是清晰的,对于应用程序的哪一层函数出错了有足够的信息。这样的错误输出有别于难以理解的异常堆栈跟踪,程序员可以添加友好的可读上下文描述这些错误的原因,并且通过清晰错误链进行处理。

这种错误链自然成为了标准 Golang 程序结构的一部分,可能看起来像这样:

// In controllers/user.go
if err := db.CreateUser(user); err != nil { return fmt.Errorf("could not create user: %w", err)
}

// In database/user.go
func (db *Database) CreateUser(user *User) error { ok, err := db.DoesUserExist(user) if err != nil { return fmt.Errorf("could not check if user already exists in db: %w", err) } ...
}

func (db *Database) DoesUserExist(user *User) error { if err := db.Connected(); err != nil { return fmt.Errorf("could not establish db connection: %w", err) } ...
}

func (db *Database) Connected() error { if !hasInternetConnection() { return errors.New("no internet connection") } ...
}

  
 
  • 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

并且,如果你还希望将堆栈跟踪附加到函数中,可以使用 github.com/pkg/errors 库,它将打印出堆栈跟踪以及开发者构建的错误链。

errors.Wrapf(err, "could not save user with email %s", email)

  
 
  • 1

示例:

package main

import ( "fmt"
)

type divideError struct { dividee int divider int
}

func (de *divideError) Error() string { strFormat := ` Cannot proceed, the divider is zero. dividee: %d divider: 0
` return fmt.Sprintf(strFormat, de.dividee)
}

func divide(varDividee int, varDivider int) (result int, errorMsg string) { if varDivider == 0 { dData := divideError{ dividee: varDividee, divider: varDivider, } errorMsg = dData.Error() return 0, errorMsg } return varDividee / varDivider, ""
}

func main() { if result, errorMsg := divide(100, 10); errorMsg == "" { fmt.Println("100/10 = ", result) } if _, errorMsg := divide(100, 0); errorMsg != "" { fmt.Println("errorMsg is: ", errorMsg) }

}

  
 
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

错误处理策略

  1. 将错误返回给调用者。
resp, err := http.Get(url) 

if err != nil{ return nill, err 
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 构造新的信息返回给调用者。
doc, err := html.Parse(resp.Body) 
resp.Body.Close()

if err != nil { return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在 main() 函数中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了 bug,才能在库函数中结束程序。
if err := WaitForServer(url); err != nil { 
	fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) 
	os.Exit(1)
}

  
 
  • 1
  • 2
  • 3
  • 4
  1. 有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过 log 包提供函数,或者标准错误流输出错误信息。
if err := Ping(); err != nil { log.Printf("ping failed: %v; networking disabled",err)
}
// or
if err := Ping(); err != nil { fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 我们可以直接忽略掉错误。
dir, err := ioutil.TempDir("", "scratch")
if err != nil { return fmt.Errorf("failed to create temp dir: %v",err) 
}

// ...use temp dir...
os.RemoveAll(dir)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

尽管 os.RemoveAll 会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。

我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该在清晰的记录下你的意图。

建议

  1. 当你的错误需要服务于开发人员时,建议添加堆栈跟踪。
  2. 对返回的错误进行处理,不要只是将它们忽略。
  3. 保持错误链的友好且明确。

文章来源: is-cloud.blog.csdn.net,作者:范桂飓,版权归原作者所有,如需转载,请联系作者。

原文链接:is-cloud.blog.csdn.net/article/details/107165165

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。