英文:
How do I represent an Optional String in Go?
问题
我希望对一个可能有两种形式的值进行建模:不存在或者是一个字符串。
在Go中,通常的做法是使用Maybe String
、Optional<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<String>
, 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
),然后你可以将string
和bool
打包到一个struct
中,这就是包装器,但这并不适用于“仅使用string
”的情况(但仍然是一个可行的解决方案)。
如果我们想坚持只使用string
,我们可以从string
类型的可能值中排除一个特定的值(长度没有限制,可能值有“无限”多个(或许长度是有限的,因为它必须是一个int
,但这没关系)),我们可以将这个特定的值命名为null
值,表示“不是一个字符串”。
表示null
的最方便的值是string
的零值,即空字符串:""
。将其指定为null
元素的好处是,每当你创建一个string
变量而没有显式指定初始值时,它将被初始化为""
。而且,当从一个值为string
的map
中查询一个元素时,如果键不在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
: ""
. 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 ""
. Also when querying an element from a map
whose value is string
will also yield ""
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 = "\xff"
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 = "\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
}
答案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 := &"foo"
unfortunately. But there is a workaround for this:
func strPtr(s string) *string {
return &s
}
Calls to strPtr("foo")
should be inlined, so it's effectively &"foo"
.
Another possibility is to use new
:
sp := new(string)
*sp = "foo"
答案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
请注意,接口是通过复制进行赋值的,因此 a
和 b
是不同的。
英文:
With an interface type, you can use a more natural assignment syntax.
var myString interface{} // used as type <string>
myString = nil // nil is the default -- and indicates 'empty'
myString = "a value"
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("myString:",myString) // prints the value (or "<nil>")
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{} = "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)
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("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{})
}
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{}
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论