我如何在 Go 中表示可选字符串?

How do I represent an Optional String in Go?

我希望建模一个可以有两种可能形式的值:不存在或字符串。

自然的方法是使用 Maybe String,或 Optional<String>,或 string option,等等。但是,Go 没有这样的变体类型。

然后我想,根据 Java、C 等,替代方案是可空性,或者 Go 中的 nil。但是,nil 不是 Go 中 string 类型的成员。

搜索,然后想到使用*string类型。这可以工作,但看起来很尴尬(例如,我不能像获取结构文字的地址那样获取字符串文字的地址)。

在 Go 中为这样的值建模的惯用方法是什么?

您可以为此使用 sql.NullString, but I personally would stick to *string. As for awkwardness, it's true that you can't just sp := &"foo" unfortunately. But there is a 之类的东西:

func strPtr(s string) *string {
    return &s
}

strPtr("foo") 的调用应该是内联的,所以它实际上是 &"foo".

另一种可能性是使用 new:

sp := new(string)
*sp = "foo"

一个合乎逻辑的解决方案是使用 Ainar-G 提到的 *string 详细说明了获取指向值的指针的可能性(int64 但同样适用于 string)。包装器是另一种解决方案。

仅使用 string

可选的 string 表示 string 加上 1 个特定值(或状态)表示 "not a string"(但 null)。

这 1 个特定值可以存储(发出信号)在另一个变量中(例如 bool),您可以将 stringbool 打包到 struct我们到达了包装器,但这不适合 "using just a string" 的情况(但仍然是一个可行的解决方案)。

如果我们只想坚持 string,我们可以从 string 类型的可能值中取出 1 个特定值(其中有 "infinity" 个可能值作为长度不受限制(或者可能是因为它必须是 int 但没关系)),我们可以将这个特定值命名为 null 值,该值表示 "not a string" .

表示null最方便的值是string的零值,也就是空的string""。将此指定为 null 元素的便利之处在于,每当您创建一个 string 变量而未明确指定初始值时,它将使用 "" 进行初始化。此外,当从 map 中查询值为 string 的元素时,如果键不在 map.

中,也会产生 ""

此解决方案适用于许多现实生活中的用例。例如,如果可选的 string 应该是一个人的名字,一个空的 string 并不真正意味着一个有效的人名,所以你不应该首先允许它。

当然,在某些情况下,空 string 确实表示 string 类型变量的有效值。对于这些用例,我们可以选择另一个值。

在 Go 中,string 实际上是一个只读字节片。请参阅博客 post Strings, bytes, runes and characters in Go,其中详细解释了这一点。

所以 string 是一个字节片,在有效文本的情况下是 UTF-8 编码字节。假设您想在可选的 string 中存储一个有效文本(如果您不想,那么您可以只使用 []byte 而不是它可以具有 nil 值),您可以选择string 值表示无效的 UTF-8 字节序列,因此您甚至不必做出妥协来从可能的值中排除有效文本。最短的无效 UTF-8 字节序列只有 1 个字节,例如 0xff(还有更多)。注意:您可以使用 utf8.ValidString() 函数来判断 string 值是否为有效文本(有效的 UTF-8 编码字节序列)。

您可以将此异常值设置为 const:

const Null = "\xff"

这么短也意味着检查 string 是否等于这个会非常快。
按照这个约定,你已经有了一个可选的 string ,它也允许空的 string.

Go Playground 上试用。

const Null = "\xff"

func main() {
    fmt.Println(utf8.ValidString(Null)) // false

    s := Null
    fmt.Println([]byte(s)) // [255]
    fmt.Println(s == Null) // true
    s = "notnull"
    fmt.Println(s == Null) // false
}

对于接口类型,您可以使用更自然的赋值语法。

var myString interface{} // used as type <string>
myString = nil // nil is the default -- and indicates 'empty'
myString = "a value"

引用值时,通常需要 才能明确检查。

// checked type assertion
if s, exists := myString.(string); exists {
    useString(s)
}

此外,由于 stringers,在某些情况下 'optional' 类型将被自动处理——这意味着您不需要显式转换值。 fmt 包使用此功能:

fmt.Println("myString:",myString) // prints the value (or "<nil>")

警告

赋值时没有类型检查。

在某些方面,这是比处理指针更简洁的方法。但是,因为它使用接口类型,所以它不限于持有特定的底层类型。风险在于您可能无意中分配了不同的类型——这将被视为与上述条件中的 nil 相同。

下面是使用接口进行赋值的演示:

var a interface{} = "hello"
var b = a // b is an interface too
b = 123 // assign a different type

fmt.Printf("a: (%T) %v\n", a, a)
fmt.Printf("b: (%T) %v\n", b, b)

输出:

a: (string) hello
b: (int) 123

请注意,接口是通过重复分配的,因此 ab 是不同的。

你可以使用 Go 惯用的接口:

type (
    // An interface which represents an optional string.
    StrOpt interface{ implStrOpt() }

    StrOptVal  string   // A string value for StrOpt interface.
    StrOptNone struct{} // No value for StrOpt interface.
)

func (StrOptVal) implStrOpt()  {} // implement the interface
func (StrOptNone) implStrOpt() {}

这就是您的使用方式:

func Foo(maybeName StrOpt) {
    switch val := maybeName.(type) {
    case StrOptVal:
        fmt.Printf("String value! -> %s\n", string(val))
    case StrOptNone:
        fmt.Println("No value!")
    default:
        panic("StrOpt does not accept a nil value.")
    }
}

func main() {
    Foo(StrOptVal("hello world"))
    Foo(StrOptNone{})
}

playground 中测试它。