go 建议:错误:在可能的情况下,用类型化的常量替换哨兵错误,

0mkxixxg  于 6个月前  发布在  Go
关注(0)|答案(6)|浏览(45)

哨兵错误是一种常见的错误处理模式,涉及与全局变量的比较。标准库中的 oserror 包提供了一个很好的例子,展示了如何通常定义这些:

package oserror

import "errors"

var (
	ErrInvalid    = errors.New("invalid argument")
	ErrPermission = errors.New("permission denied")
	ErrExist      = errors.New("file already exists")
	ErrNotExist   = errors.New("file does not exist")
	ErrClosed     = errors.New("file already closed")
)

它们具有全局变量的所有常见警告,即它们可以被重新分配:

package main

import (
	"fmt"
	"os"
)

func main() {
	var nilFile *os.File

	// prints 0, error("invalid argument")
	fmt.Println(nilFile.Read(nil))

	// allowed :(
	os.ErrInvalid = nil

	// segfaults due to an internal error check being bypassed
	fmt.Println(nilFile.Read(nil))
}

使用类型化常量定义这些的替代方法(向后兼容?)是可能的:

package oserror

// no imports needed! :)

// unexported error type means that no other package
// can define an error equal to any of the below constants,
// even if the string values match.
type osError string

func (err osError) Error() string {
	return string(err)
}

// constants can't be reassigned :)
const (
	// errors are unique as long as the strings are unique.
	ErrInvalid    = osError("invalid argument")
	ErrPermission = osError("permission denied")
	ErrExist      = osError("file already exists")
	ErrNotExist   = osError("file does not exist")
	ErrClosed     = osError("file already closed")
)

我还没有考虑这种方法的潜在缺点或长期影响。

50few1ms

50few1ms1#

谢谢,这是一个好主意。但是我们现在不能做出这样的改变,因为它会破坏Go 1的兼容性保证。

kmbjn2e3

kmbjn2e32#

我认为一个更好的替代方案,可以省略分配,是使用零大小的专用类型:

type errInvalid struct{}

func (errInvalid) Error() string {
	return "invalid argument"
}

type errPermission struct{}

func (errPermission) Error() string {
	return "permission denied"
}

type errExist struct{}

func (errExist) Error() string {
	return "file already exists"
}

type errNotExist struct{}

func (errNotExist) Error() string {
	return "file does not exist"
}

type errClosed struct{}

func (errClosed) Error() string {
	return "file already closed"
}

var (
	ErrInvalid    errInvalid
	ErrPermission errPermission
	ErrExist      errExist
	ErrNotExist   errNotExist
	ErrClosed     errClosed
)

从技术上讲,可以将它们重新分配, os.ErrInvalid = os.ErrInvalid 是有效的代码,但是它什么都不做,所以没问题,每次有人 returnerrors.Is 错误值时都避免分配。Nvm将常量字符串作为接口传递时,会将字符串头分配为全局变量,但我仍然认为像 var ErrInvalid errors.Error[osError, "invalid argument"] 这样的东西更好。
能找到一种不那么冗长的表达方式就好了。
如果我们可以将 const 作为类型参数传递,就可以这样做:

type errInvalid errors.Error["invalid argument"]
var ErrInvalid errInvalid

这样的辅助函数可以轻松地创建 Package 父错误以进行通配符检查的单独错误:

type Error[Wrapping error, Message const string] struct{}

func (Error[Wrapping, Message]) Error() string {
 return Message
}

func (Error[Wrapping, Message]) Unwrap() error {
 var zero Wrapping
 return zero
}
7vux5j2d

7vux5j2d3#

使用常量字符串错误的一个缺点是,我们可以很容易地发明出相同类型的新错误:

package main

type osError string

func (err osError) Error() string {
	return string(err)
}

const ErrInvalid = osError("invalid argument")

func main() {
	var myError error = ErrInvalid + " 2"
	println(myError.Error())
}

也许,添加一个验证规则是个好主意:查找所有对全局错误值的修改(包括获取全局错误值地址的操作)。

drnojrws

drnojrws4#

如果我们能将 const 作为类型参数,就可以这样写:

type errInvalid errors.Error["invalid argument"]
var ErrInvalid errInvalid

此外,已经有关于 const 类型参数的先例:数组

var (
	a [4]int
	b [3]float32
	c [8]string
)
f2uvfpb9

f2uvfpb95#

Without getting too off track, has Go considered adding a val qualifier to ensure the variable can't be re-assigned post initialization ? I'd be interested in reading a thread about it if there is one. I've read through several proposals about adding immutable types and found myself agreeing with the arguments against it, but it seems like preventing re-assignment and leaving interior mutability to the type (i.e no setters) might be middle ground solution that could apply to more than just errors.

相关问题