Golang中这样处理错误是个好主意吗?

Golang中这样处理错误是个好主意吗? 大家好。

我想和大家分享一个我遇到的问题,我找到的解决方案,并询问你们从Go语言的角度来看,我的解决方案是优雅还是拙劣。

问题

首先说明一下,我必须处理字节数据。我需要将字节分成许多小部分来写入。为此我使用了io.Writer。如你所知,它有一个Write方法,该方法返回写入的字节数和错误

这就是主要问题。我必须在每次写入操作后检查错误。而且,我没有任何处理错误的手段;一旦发生错误,就结束了。必须处理每个错误(我有许多Write调用!)让人难以忍受地繁琐和笨重。

解决方案

一天后,我想出了一个解决方案。我创建了一个未导出的符合io.Writer接口的结构体panicWriter

import "io"

type panicWriter struct {
    realWriter io.Writer
}

func (pw panicWriter) Write(data []byte) (n int, err error) {
    n, err = pw.realWriter.Write(data)
    if err != nil {
        panic(err)
    }

    return
}

现在,我只需在最上层的写入函数中(我正在编写一个库)延迟一个恢复函数,该函数设置错误返回值,这样我现在可以利用io.Writer的所有优势而无需检查返回的错误。因此,从技术上讲,这是在库内模拟异常处理,这样panic就不会离开库。


那么,这是一个好的决定吗?可能会有哪些隐患呢?我两周前开始学习Go,可能仍然误解了它的哲学。

这可能是一个愚蠢的问题,但我坚信新程序员应该尽快了解新语言的可行与不可行之处。


更多关于Golang中这样处理错误是个好主意吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

这是个好主意,应该适合我。谢谢!

更多关于Golang中这样处理错误是个好主意吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


好的!也许你可以按照视频中的约定将其重命名为 SafeWriter(它不再引发 panic,所以 panicWriter 有点误导性 😉)

是的,当你希望错误自行传播时,这很有帮助。但在我的情况下,panic 发生在 Writer 接口的方法内部,而我可以将该接口的对象发送到其他库,这时就变得危险了。

我在想,如果我使用某个封装了这个 writer 的库(假设叫 X)会发生什么?如果库 X 内部使用 panic-recover 模式来简化错误处理会怎样?

我认为这会导致库 X 内部出现误报,panicWriter 可能会在其中失败。我觉得我无法预测接下来会发生什么。

是的,@kylewood 你说得对。当然在某些情况下返回错误是不切实际的,使用 panic 能让逻辑更加清晰。在你引用的页面中也提到:

虽然这种模式很有用,但应该仅在包内部使用。Parse 将其内部的 panic 调用转换为 error 值;它不会向客户端暴露 panics。这是一个值得遵循的好规则。

我认为对于来自 Python、Java 或其他具有异常处理机制语言的 Go 初学者来说,很容易寻找某种异常处理方式,然后发现并过度使用 panic。

我认为将panic用作类似异常的机制是不可取的。它应该被保留用于不可恢复的错误。

我不同意这种观点。panic的一个核心特性及其实用性的关键因素在于,它们会破坏堆栈直到被恢复,这与异常非常相似。Effective Go中关于恢复的章节对此进行了讨论。库必须捕获panic且绝不将其暴露给调用者,但只要满足这一条件,它就是跳出复杂函数调用栈而不必在每个函数调用中都返回错误的好方法。

想象一个解析器,它通常具有非常间接的递归。也就是说,像 A -> B -> C -> D -> B -> C -> A -> B -> C 这样的调用栈非常常见。在这种情况下,当发生错误时,panic 是直接返回到最顶层函数调用的完美方式。这与Effective Go中给出的示例类似。

我认为不应该将 panic 用作类似异常的机制。它应该被保留用于不可恢复的错误。

https://gobyexample.com/panic

panic 通常意味着发生了意外的错误。大多数情况下,我们用它来快速处理在正常操作中不应发生或我们未准备好优雅处理的错误。

你可以按照这个视频中的建议来实现:https://www.youtube.com/watch?v=1B71SL6Y0kA,类似这样:

package main

import (
	"fmt"
	"io"
	"os"
)

type panicWriter struct {
	realWriter io.Writer
	err        error
}

func (pw panicWriter) Write(data []byte) (n int) {
	if pw.err != nil {
		return
	}
	n, pw.err = pw.realWriter.Write(data)
	return
}

func main() {
	p := panicWriter{realWriter: os.Stdout}
	p.Write([]byte("Hello"))
	p.Write([]byte(" world"))
	p.Write([]byte("!"))

	if p.err != nil {
		// 处理错误
		fmt.Println("Error:", p.err)
	}
}

在Go语言中,你的解决方案存在显著问题,违背了Go的错误处理哲学。Go明确设计为通过多返回值显式处理错误,而不是依赖panic/recover机制作为常规错误处理方式。以下是具体分析:

问题分析

  1. 掩盖了错误处理的本质:Go的核心设计原则是错误应该是显式的、可预测的
  2. panic不是为常规错误设计的:panic应该用于不可恢复的程序错误,而不是预期的I/O错误
  3. 性能开销:panic/recover比普通的错误检查有更大的性能代价

更好的解决方案

方案1:使用错误聚合

type bufferedWriter struct {
    w   io.Writer
    err error
}

func (bw *bufferedWriter) Write(data []byte) (int, error) {
    if bw.err != nil {
        return 0, bw.err
    }
    
    n, err := bw.w.Write(data)
    if err != nil {
        bw.err = err
    }
    return n, err
}

// 使用方式
func processData(w io.Writer, chunks [][]byte) error {
    bw := &bufferedWriter{w: w}
    
    for _, chunk := range chunks {
        bw.Write(chunk)
    }
    
    return bw.err
}

方案2:使用现有的库函数

import "io"

// 使用io.Copy或io.WriteString等已经处理了多次写入的函数
func writeAll(w io.Writer, chunks [][]byte) error {
    for _, chunk := range chunks {
        if _, err := w.Write(chunk); err != nil {
            return err
        }
    }
    return nil
}

方案3:使用helper函数减少样板代码

func mustWrite(w io.Writer, data []byte) {
    if _, err := w.Write(data); err != nil {
        // 记录日志或处理错误
        log.Printf("write failed: %v", err)
        // 或者返回错误
    }
}

为什么应该避免panic

// 这种模式在Go中是不推荐的
func riskyOperation(w io.Writer) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    
    pw := panicWriter{realWriter: w}
    // 多个写入操作...
    pw.Write([]byte("data"))
    
    return nil
}

Go社区普遍认为显式的错误处理虽然繁琐,但带来了更好的代码可读性和可维护性。你的panicWriter方案虽然解决了重复错误检查的问题,但引入了更大的复杂性和不符合语言习惯的做法。

回到顶部