How do I represent an Optional String in Go?

huangapple go评论81阅读模式
英文:

How do I represent an Optional String in Go?

问题

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

在Go中,通常的做法是使用Maybe StringOptional<String>string option等方式。然而,Go并没有像这样的变体类型。

然后我想到,像Java、C等语言一样,可以使用nullability或者在Go中使用nil。然而,在Go中,nil不是string类型的成员。

经过搜索,我发现可以使用*string类型。这可能可以工作,但似乎非常笨拙(例如,我不能像取结构体字面量的地址那样取字符串字面量的地址)。

在Go中,如何以惯用的方式对这样的值进行建模?

英文:

I wish to model a value which can have two possible forms: absent, or a string.

The natural way to do this is with Maybe String, or Optional&lt;String&gt;, or string option, etc. However, Go does not have variant types like this.

I then thought, following Java, C, etc., that the alternative would be nullability, or nil in Go. However, nil is not a member of the string type in Go.

Searching, I then thought to use the type *string. This could work, but seems very awkward (e.g. I cannot take the address of the string literal in the same way that I can take the address of a struct literal).

What is the idiomatic way to model such a value in Go?

答案1

得分: 17

一个合理的解决方案是使用*string,正如Ainar-G所提到的。这个答案详细介绍了获取值的指针(int64,但对于string也适用)的可能性。另一种解决方案是使用包装器。

仅使用string

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

这个特定的值可以存储(标记)在另一个变量中(例如bool),然后你可以将stringbool打包到一个struct中,这就是包装器,但这并不适用于“仅使用string”的情况(但仍然是一个可行的解决方案)。

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

表示null的最方便的值是string的零值,即空字符串:""。将其指定为null元素的好处是,每当你创建一个string变量而没有显式指定初始值时,它将被初始化为""。而且,当从一个值为stringmap中查询一个元素时,如果键不在map中,也会返回""

这个解决方案适用于许多实际应用场景。例如,如果可选的string应该是一个人的名字,那么空字符串实际上并不表示一个有效的人名,所以你不应该允许这种情况发生。

当然,可能会有一些情况,空字符串确实代表string类型变量的一个有效值。对于这些用例,我们可以选择另一个值。

在Go中,string实际上是一个只读的字节切片。参见博文Go中的字符串、字节、符文和字符,其中详细解释了这一点。

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

你可以将这个特殊值定义为const

const Null = "\xff"

由于这个值很短,所以检查一个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
}
英文:

A logical solution would be to use *string as mentioned by Ainar-G. This other answer details the possibilities of obtaining a pointer to a value (int64 but the same works for string too). A wrapper is another solution.

Using just string

An optional string means a string plus 1 specific value (or state) saying "not a string" (but a null).

This 1 specific value can be stored (signaled) in another variable (e.g bool) and you can pack the string and the bool into a struct and we arrived to the wrapper, but this doesn't fit into the case of "using just a string" (but is still a viable solution).

If we want to stick to just a string, we can take out 1 specific value from the possible values of a string type (which has "infinity" possible values as the length is not limited (or maybe it is as it must be an int but that's all right)), and we can name this specific value the null value, the value which means "not a string".

The most convenient value for indicating null is the zero value of string, which is the empty string: &quot;&quot;. Designating this the null element has the convenience that whenever you create a string variable without explicitly specifying the initial value, it will be initialized with &quot;&quot;. Also when querying an element from a map whose value is string will also yield &quot;&quot; if the key is not in the map.

This solution suits many real-life use-cases. If the optional string is supposed to be a person's name for example, an empty string does not really mean a valid person name, so you shouldn't allow that in the first place.

There might be cases of course when the empty string does represent a valid value of a variable of string type. For these use-cases we can choose another value.

In Go, a string is in effect a read-only slice of bytes. See blog post Strings, bytes, runes and characters in Go which explains this in details.

So a string is a byte slice, which is the UTF-8 encoded bytes in case of a valid text. Assuming you want to store a valid text in your optional string (if you wouldn't, then you can just use a []byte instead which can have a nil value), you can choose a string value which represents an invalid UTF-8 byte sequence and thus you won't even have to make a compromise to exclude a valid text from the possible values. The shortest invalid UTF-8 byte sequence is 1 byte only, for example 0xff (there are more). Note: you can use the utf8.ValidString() function to tell if a string value is a valid text (valid UTF-8 encoded byte sequence).

You can make this exceptional value a const:

const Null = &quot;\xff&quot;

Being this short also means it will be very fast to check if a string equals to this.
And by this convention you already have an optional string which also allows the empty string.

Try it on the Go Playground.

const Null = &quot;\xff&quot;

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

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

答案2

得分: 11

你可以使用类似 sql.NullString 的东西,但我个人更倾向于使用 *string。至于笨拙的地方,确实不能直接 sp := &"foo",但是有一个解决方法

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

调用 strPtr("foo") 应该会被内联,所以实际上就是 &"foo"

另一种可能性是使用 new

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

You could use something like sql.NullString, but I personally would stick to *string. As for awkwardness, it's true that you can't just sp := &amp;&quot;foo&quot; unfortunately. But there is a workaround for this:

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

Calls to strPtr(&quot;foo&quot;) should be inlined, so it's effectively &amp;&quot;foo&quot;.

Another possibility is to use new:

sp := new(string)
*sp = &quot;foo&quot;

答案3

得分: 2

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

var myString interface{} // 用作类型 <string>
myString = nil // nil 是默认值,表示“空”
myString = "a value"

在引用值时,通常需要使用类型断言来明确进行检查。

// 检查类型断言
if s, exists := myString.(string); exists {
    useString(s)
}

此外,由于 stringer 的存在,有些情况下“可选”类型将自动处理,这意味着您不需要显式地转换值。fmt 包使用了这个特性:

fmt.Println("myString:", myString) // 打印值(或 "<nil>")

警告

在赋值时没有类型检查。

在某些方面,这比处理指针更清晰。然而,由于使用了接口类型,它不限于持有特定的基础类型。风险在于您可能会意外地分配不同的类型,这将与上述条件中的 nil 被视为相同。

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

var a interface{} = "hello"
var b = a // b 也是一个接口
b = 123 // 分配不同的类型

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

输出:

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

请注意,接口是通过复制进行赋值的,因此 ab 是不同的。

英文:

With an interface type, you can use a more natural assignment syntax.

var myString interface{} // used as type &lt;string&gt;
myString = nil // nil is the default -- and indicates &#39;empty&#39;
myString = &quot;a value&quot;

When referencing the value, a type assertion is typically required to make the checking explicit.

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

Also, because of stringers there are some contexts in which the 'optional' type will be handled automatically -- meaning that you don't need to explicitly cast the value. The fmt package uses this feature:

fmt.Println(&quot;myString:&quot;,myString) // prints the value (or &quot;&lt;nil&gt;&quot;)

Warning

There is no type-checking when assigning to the value.

In some ways, this is a cleaner approach than dealing with pointers. However, because this uses an interface type, it is not limited to holding a specific underlying type. The risk is that you could unintentionally assign a different type -- which would be treated the same as nil in the above conditional.

Here's a demonstration of assignment using interfaces:

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

fmt.Printf(&quot;a: (%T) %v\n&quot;, a, a)
fmt.Printf(&quot;b: (%T) %v\n&quot;, b, b)

Output:

<pre>
a: (string) hello
b: (int) 123
</pre>

Notice that interfaces are assigned by duplication, so a and b are distinct.

答案4

得分: 1

你可以使用一个符合Go语言惯用法的接口:

type (
    // 表示可选字符串的接口
    StrOpt interface{ implStrOpt() }

    StrOptVal  string   // StrOpt接口的字符串值
    StrOptNone struct{} // StrOpt接口的无值
)

func (StrOptVal) implStrOpt()  {} // 实现接口
func (StrOptNone) implStrOpt() {}

这是如何使用它的示例

func Foo(maybeName StrOpt) {
    switch val := maybeName.(type) {
    case StrOptVal:
        fmt.Printf("字符串值! -> %s\n", string(val))
    case StrOptNone:
        fmt.Println("无值!")
    default:
        panic("StrOpt不接受nil值。")
    }
}

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

[playground](https://go.dev/play/p/RJo_tDCqZVQ)中进行测试。
英文:

You can use an interface, which is idiomatic 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() {}

And this is how you use it:

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

func main() {
    Foo(StrOptVal(&quot;hello world&quot;))
    Foo(StrOptNone{})
}

Test it in the playground.

答案5

得分: 0

有一个目录化的开源解决方案,适用于原始类型,受到了Java的Optional的启发。

package optional 的官方文档在这里:
https://pkg.go.dev/github.com/markphelps/optional

给出的动机是:

在Go中,没有明确初始值声明的变量会被赋予它们的零值。大多数情况下这是你想要的,但有时你想要知道一个变量是否被设置或者它只是一个零值。这就是可选类型派上用场的地方。

optional.String 为例,它具有以下接口:

type String
func NewString(v string) String
func (s String) Get() (string, error)
func (s String) If(fn func(string))
func (s String) MarshalJSON() ([]byte, error)
func (s String) MustGet() string
func (s String) OrElse(v string) string
func (s String) Present() bool
func (s *String) Set(v string)
func (s *String) UnmarshalJSON(data []byte) error

最新的经过GitHub验证(签名)的发布是 v0.10.0,发布日期为2022年6月4日。

go get -u github.com/markphelps/optional/cmd/optional@v0.10.0

文档中的一个细节:要取消定义一个值,可以赋值{}optional.String{}

英文:

There is a cataloged open source solution, for primitive types, inspired by 'Java Optional'.

Official documentation for package optional is here:
https://pkg.go.dev/github.com/markphelps/optional

The motivation given is:

> In Go, variables declared without an explicit initial value are given their zero value. Most of the time this is what you want, but sometimes you want to be able to tell if a variable was set or if it's just a zero value. That's where option types come in handy.

For optional.String, as an example, it has the following interface:

type String
func NewString(v string) String
func (s String) Get() (string, error)
func (s String) If(fn func(string))
func (s String) MarshalJSON() ([]byte, error)
func (s String) MustGet() string
func (s String) OrElse(v string) string
func (s String) Present() bool
func (s *String) Set(v string)
func (s *String) UnmarshalJSON(data []byte) error

The latest GitHub verified (signed) release is v0.10.0 on Jun 4, 2022.

go get -u github.com/markphelps/optional/cmd/optional@v0.10.0

A subtlety in the documentation: To undefine a value, assign {} or optional.String{}.

huangapple
  • 本文由 发表于 2015年6月9日 20:10:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/30731687.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定