【问题标题】:How do I represent an Optional String in Go?如何在 Go 中表示可选字符串?
【发布时间】:2015-06-09 12:10:53
【问题描述】:

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

执行此操作的自然方法是使用Maybe StringOptional<String>string option 等。但是,Go 没有这样的变体类型。

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

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

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

【问题讨论】:

  • 相关:stackoverflow.com/a/30716481/1705598。它适用于*int64,但您可以对*string 使用相同的解决方案。
  • 在您的应用程序中,未设置(不存在)的字符串与“”之间是否存在语义差异?大多数时候,根据我的经验,你可以有效地对待他们。空字符串"" 是 go 中字符串的零值。这是惯用的,但也许您需要这种区别?
  • @SkyeC 的问题在于它没有表达类型的可选性;这只是一个不可靠的约定。

标签: string go null optional variant


【解决方案1】:

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

只使用string

可选的string 表示string 加上1 个特定值(或状态)表示“不是字符串”(而是null)。

这 1 个特定值可以存储(信号)在另一个变量中(例如bool),您可以将stringbool 打包到struct 中,我们到达了包装器,但这并没有'不适合“仅使用string”的情况(但仍然是一个可行的解决方案)。

如果我们只想坚持 string,我们可以从 string 类型的可能值中取出 1 个特定值(它具有“无限”可能值,因为长度不受限制(或者可能是因为它必须是int 但没关系)),我们可以将这个特定值命名为null 值,该值表示“不是字符串”。

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

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

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

在 Go 中,string 实际上是只读的字节片。请参阅博客文章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
}

【讨论】:

  • 但是,使用这种方法,我们可能会丢失 s 可能为空的上下文。
【解决方案2】:

您可以使用sql.NullString 之类的东西,但我个人会坚持使用*string。至于尴尬,不幸的是你不能只是sp := &"foo"。但是有一个workaround

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

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

另一种可能是使用new:

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

【讨论】:

  • 谢谢。看起来指针解决方案和我得到的一样好,这令人失望。为什么哦为什么在 21 世纪语言仍然没有变体类型/总和类型/ADT。
  • Optional<String> 类型在使用异常处理错误的语言中增加了价值。然而,Golang 通过返回值明确错误处理。
  • @MichalČizmazia 语言如 Haskell 使用 Maybe(也就是 Optional 的另一个名字)并且在正常情况下不要使用异常来处理错误。事实上,Optional 类型与异常无关,是编译时检查(与异常相反)。
  • @AndresF。你说的对!函数式编程需要Optional 远远超出我所指的getOrThrow 操作。
  • @jameshfisher 我推荐使用 sql.NullString,字符串指针可能是一个可怕的东西,你需要检查 nil 并在任何地方都遵守它,并且可能导致意外的 nil 崩溃。
【解决方案3】:

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

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

引用该值时,通常需要type assertion 来明确检查。

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

另外,由于stringers,在某些上下文中“可选”类型将被自动处理——这意味着您不需要显式转换值。 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:(字符串)你好 b:(整数)123

注意接口是通过重复分配的,所以ab 是不同的。

【讨论】:

    【解决方案4】:

    你可以使用一个接口,这是惯用的 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 中测试它。

    【讨论】:

      猜你喜欢
      • 2014-10-07
      • 2023-03-08
      • 2022-11-11
      • 2016-08-11
      • 2020-12-10
      • 2019-02-14
      • 1970-01-01
      • 1970-01-01
      • 2010-12-20
      相关资源
      最近更新 更多