错误处理的高级技巧
如果你写过 Go 代码,一定对这样的代码不陌生:
result, err := doSomething()
if err != nil {
return err
}
Go 的错误处理方式一直备受争议。有人认为它太啰嗦,有人认为它很清晰。不管你怎么看,Go 1.13 引入的错误包装机制让错误处理变得更加优雅和强大。
传统的错误处理
在 Go 1.13 之前,我们通常这样处理错误:
package main
import (
"errors"
"fmt"
)
// 定义哨兵错误
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrNotFound
}
// 查找用户逻辑
return &User{ID: id, Name: "Alice"}, nil
}
func main() {
user, err := findUser(-1)
if err != nil {
if err == ErrNotFound {
fmt.Println("User not found")
} else {
fmt.Printf("Unknown error: %v\n", err)
}
return
}
fmt.Printf("Found user: %s\n", user.Name)
}
这种方式的问题在于:当错误被层层传递时,我们会丢失上下文信息。
错误包装(Go 1.13+)
Go 1.13 引入了 fmt.Errorf 的 %w 动词,允许我们包装错误并保留错误链:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
}
return &User{ID: id}, nil
}
func getUser(id int) (*User, error) {
user, err := findUser(id)
if err != nil {
return nil, fmt.Errorf("getUser: %w", err)
}
return user, nil
}
func main() {
_, err := getUser(-1)
if err != nil {
fmt.Printf("Error: %v\n", err)
// 输出: Error: getUser: findUser: invalid id -1: not found
// 检查错误链
if errors.Is(err, ErrNotFound) {
fmt.Println("This is a not found error")
}
}
}
errors.Is 和 errors.As
errors.Is 用于检查错误链中是否包含特定错误:
if errors.Is(err, ErrNotFound) {
// 处理 not found 错误
}
errors.As 用于检查错误链中是否包含特定类型的错误:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}
func validateUser(user *User) error {
if user.Name == "" {
return &ValidationError{Field: "Name", Message: "cannot be empty"}
}
return nil
}
func main() {
err := validateUser(&User{})
if err != nil {
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Validation failed: %s - %s\n", valErr.Field, valErr.Message)
}
}
}
自定义错误类型
创建自定义错误类型可以让错误携带更多信息:
package main
import (
"fmt"
"net/http"
)
// APIError 表示 API 错误
type APIError struct {
Code int
Message string
Details map[string]interface{}
}
func (e *APIError) Error() string {
return fmt.Sprintf("API error %d: %s", e.Code, e.Message)
}
// NewNotFoundError 创建 404 错误
func NewNotFoundError(resource string) *APIError {
return &APIError{
Code: http.StatusNotFound,
Message: fmt.Sprintf("%s not found", resource),
}
}
// NewValidationError 创建 400 错误
func NewValidationError(field, message string) *APIError {
return &APIError{
Code: http.StatusBadRequest,
Message: "validation failed",
Details: map[string]interface{}{
"field": field,
"message": message,
},
}
}
// Handler 示例
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
err := NewValidationError("id", "is required")
handleError(w, err)
return
}
user, err := findUser(id)
if err != nil {
handleError(w, err)
return
}
// 返回用户
_ = user
}
func handleError(w http.ResponseWriter, err error) {
var apiErr *APIError
if errors.As(err, &apiErr) {
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
} else {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"error": "internal server error",
})
}
}
错误处理的最佳实践
1. 只在边界处处理错误
// 不好:在每一层都处理错误
func (s *Service) GetUser(id int) (*User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
log.Printf("Error finding user: %v", err) // 这里不应该 log
return nil, err
}
return user, nil
}
// 好:只在最外层处理
func (s *Service) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(id)
if err != nil {
log.Printf("Error getting user %d: %v", id, err) // 在这里 log
handleError(w, err)
return
}
// 返回用户
}
2. 添加上下文信息
// 不好:丢失上下文
func processOrder(order *Order) error {
err := validateOrder(order)
if err != nil {
return err
}
err = saveOrder(order)
if err != nil {
return err
}
return sendNotification(order)
}
// 好:添加上下文
func processOrder(order *Order) error {
if err := validateOrder(order); err != nil {
return fmt.Errorf("validate order %d: %w", order.ID, err)
}
if err := saveOrder(order); err != nil {
return fmt.Errorf("save order %d: %w", order.ID, err)
}
if err := sendNotification(order); err != nil {
return fmt.Errorf("send notification for order %d: %w", order.ID, err)
}
return nil
}
3. 使用多返回值而不是错误包装
// 不好:用错误表示不同的业务逻辑
func getUser(id int) (*User, error) {
user := db.Find(id)
if user == nil {
return nil, errors.New("user not found") // 这不是真正的错误
}
return user, nil
}
// 好:使用多返回值
func getUser(id int) (*User, bool, error) {
user, err := db.Find(id)
if err != nil {
return nil, false, err // 这是真正的错误
}
if user == nil {
return nil, false, nil // 用户不存在,但不是错误
}
return user, true, nil
}
4. 不要忽略错误
// 不好:忽略错误
result, _ := riskyOperation()
// 好:明确处理错误
result, err := riskyOperation()
if err != nil {
// 处理错误
return err
}
// 如果确实要忽略,使用 _ 并添加注释
result, _ = riskyOperation() // 忽略错误,因为...
实战:构建一个带重试的 HTTP 客户端
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
)
// RetryError 表示重试错误
type RetryError struct {
Attempts int
LastErr error
}
func (e *RetryError) Error() string {
return fmt.Sprintf("failed after %d attempts: %v", e.Attempts, e.LastErr)
}
func (e *RetryError) Unwrap() error {
return e.LastErr
}
// HTTPClient 带重试的 HTTP 客户端
type HTTPClient struct {
client *http.Client
maxRetries int
baseDelay time.Duration
}
func NewHTTPClient(maxRetries int) *HTTPClient {
return &HTTPClient{
client: &http.Client{Timeout: 30 * time.Second},
maxRetries: maxRetries,
baseDelay: time.Second,
}
}
func (c *HTTPClient) Get(ctx context.Context, url string) ([]byte, error) {
var lastErr error
for attempt := 0; attempt <= c.maxRetries; attempt++ {
if attempt > 0 {
// 指数退避
delay := c.baseDelay * time.Duration(1<<uint(attempt-1))
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
lastErr = fmt.Errorf("attempt %d: %w", attempt+1, err)
continue
}
defer resp.Body.Close()
// 只有 5xx 错误才重试
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("attempt %d: server error %d", attempt+1, resp.StatusCode)
continue
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("client error: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
lastErr = fmt.Errorf("attempt %d: read body: %w", attempt+1, err)
continue
}
return body, nil
}
return nil, &RetryError{
Attempts: c.maxRetries + 1,
LastErr: lastErr,
}
}
func main() {
client := NewHTTPClient(3)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := client.Get(ctx, "https://api.example.com/data")
if err != nil {
var retryErr *RetryError
if errors.As(err, &retryErr) {
fmt.Printf("Failed after %d attempts\n", retryErr.Attempts)
fmt.Printf("Last error: %v\n", retryErr.LastErr)
} else {
fmt.Printf("Error: %v\n", err)
}
return
}
fmt.Printf("Success: %s\n", body)
}
总结
Go 的错误处理虽然看似啰嗦,但它强制我们显式处理每一个可能的错误,这让代码更加健壮。Go 1.13 的错误包装机制让错误处理变得更加优雅:
- 使用
%w包装错误,保留错误链 - 使用
errors.Is检查特定错误 - 使用
errors.As检查特定错误类型 - 创建自定义错误类型携带更多信息
- 只在边界处处理错误,中间层只包装和传递
- 添加上下文信息,让错误更有意义
记住:错误是值,像处理其他值一样处理错误。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。