How do I represent an Optional String in Go?
Asked Answered
W

5

37

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?

Washy answered 9/6, 2015 at 12:10 Comment(4)
Related: https://mcmap.net/q/127403/-how-do-i-do-a-literal-int64-in-go. It is for *int64, but you can use the same solutions for *string.Wuhan
In your application is there a semantic difference between a string that is unset (absent) and ""? Most of the time, in my experience, you can effectively treat them the same. The empty string "" is the zero value for a string in go. This is idiomatic but maybe you need this distinction?Mccully
@Mccully the problem with that is that it does not express optionality in the types; it's just an unreliable convention.Washy
gog.Ptr() makes taking a reference much more ergonomic.Lublin
U
14

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"
Unmeriting answered 9/6, 2015 at 12:17 Comment(5)
Thanks. It does seem like the pointer solution is as good as I'll get, which is disappointing. Why oh why in the 21st century do languages still not have variant types/sum types/ADTs.Washy
The type Optional<String> adds value in languages which use exceptions for error handling. Golang makes error handling explicit via return values, however.Autoionization
@MichalČizmazia languages such as Haskell use Maybe (which is Optional by another name) and do not, under normal circumstances, use exceptions for error handling. In fact, Optional types have nothing to do with exceptions and are a compile-time check (as opposed to exceptions).Clotilda
@AndresF. you are right! Functional programming takes Optional far beyond the getOrThrow operation I was referring to.Autoionization
@Washy I recommend use sql.NullString,string pointer can be aweful thing that you need check nill and deference it everywhere,and may cause unexpected nil crash.Quarterage
W
17

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
}
Wuhan answered 9/6, 2015 at 19:36 Comment(1)
However, using this method, we might lose the context that s might be null.Kinghood
U
14

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"
Unmeriting answered 9/6, 2015 at 12:17 Comment(5)
Thanks. It does seem like the pointer solution is as good as I'll get, which is disappointing. Why oh why in the 21st century do languages still not have variant types/sum types/ADTs.Washy
The type Optional<String> adds value in languages which use exceptions for error handling. Golang makes error handling explicit via return values, however.Autoionization
@MichalČizmazia languages such as Haskell use Maybe (which is Optional by another name) and do not, under normal circumstances, use exceptions for error handling. In fact, Optional types have nothing to do with exceptions and are a compile-time check (as opposed to exceptions).Clotilda
@AndresF. you are right! Functional programming takes Optional far beyond the getOrThrow operation I was referring to.Autoionization
@Washy I recommend use sql.NullString,string pointer can be aweful thing that you need check nill and deference it everywhere,and may cause unexpected nil crash.Quarterage
E
2

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:

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

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

Elfriedeelfstan answered 14/12, 2020 at 2:7 Comment(0)
E
1

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.

Electroacoustics answered 13/10, 2021 at 12:45 Comment(0)
E
0

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/[email protected]

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

Elfriedeelfstan answered 20/3, 2023 at 14:1 Comment(1)
"It can also be used as a tool to generate option type wrappers around your own types."Elfriedeelfstan

© 2022 - 2024 — McMap. All rights reserved.