Package option implements effective and useful instruments to work
with optional types in Go. It eliminates code doubling and provides
high performance due to:
- no memory allocations
- serialization without reflection (at least for pre-generated types)
- support for basic and custom types
go install github.com/tarantool/go-option@latestYou could run the godoc server on localhost:6060 with the command:
make godoc_runAnd open the generated documentation in another terminal or use the link:
make godoc_openGenerated types follow the pattern Optional and provide methods for working with optional values:
// Create an optional with a value.
opt := option.SomeString("hello")
// Check if a value is present.
if opt.IsSome() {
value := opt.Unwrap()
fmt.Println(value)
}
// Use a default value if none.
value := opt.UnwrapOr("default")
// Encode to MessagePack.
err := opt.EncodeMsgpack(encoder)type SomeType struct {
name string
number int
}
// Create an optional with a value.
opt := option.Some(SomeType{"hello", 42})
// Check if a value is present.
if opt.IsSome() {
value := opt.Unwrap()
fmt.Println(value)
}
// Use a default value if none.
value := opt.UnwrapOr(SomeType{"default", 0})
// Encode to MessagePack.
err := opt.EncodeMsgpack(encoder)It may be necessary to use an optional type in a structure. For example, to distinguish between a nil value and a missing one.
package main
import (
"github.com/tarantool/go-option"
tarantool "github.com/tarantool/go-tarantool/v2"
)
type User struct {
// may be used in conjunciton with 'msgpack:",omitempty"' directive to skip fields
_msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused
Name string
Phone option.String
}
func main() {
var conn *tarantool.Doer
// Initialize tarantool connection
// Imagine you get a slice of users from Tarantool.
users := []User{
{
Name: "Nosipho Nnenne",
Phone: option.SomeString("+15056463408"),
},
{
Name: "Maryamu Efe",
Phone: option.NoneString(),
},
{
Name: "Venera Okafor",
},
}
for id, user := range users {
conn.Do(
tarantool.NewInsertRequest("users").Tuple(user),
)
}
}Every optional type — the builtin ones, option.Generic[T] and the ones
produced by gentypes — implements json.Marshaler and json.Unmarshaler:
- an absent value (None) is encoded as
null, andnulldecodes as None; - a present value (Some) is encoded exactly as
json.Marshalencodes the wrapped value, taken by pointer, soMarshalJSONandMarshalTextmethods with a pointer receiver are honoured. A present zero value stays distinct from None:option.SomeInt(0)is0, notnull.
A present value whose own JSON is null (a nil slice, map, pointer or
interface) is encoded as null as well, so it decodes back as None.
Use the omitzero tag option (Go 1.24+) to drop absent values from the
output: it calls IsZero(), which reports absence. omitempty has no
effect on optional types, since they are structs.
type User struct {
Name string `json:"name"`
Phone option.String `json:"phone"`
Email option.String `json:"email,omitzero"`
}
data, _ := json.Marshal(User{Name: "Maryamu Efe"})
// {"name":"Maryamu Efe","phone":null}Optional types implement no MarshalText: there is no text for an absent
value that differs from a present empty one.
Every optional type also implements MarshalYAML() (any, error) and
UnmarshalYAML(func(any) error) error — the interfaces
gopkg.in/yaml.v3 (and yaml.v2) accept without the package being
imported, so go-option does not depend on any YAML library:
- an absent value (None) is encoded as
null; - a present value (Some) is handed to the encoder as a pointer to the
wrapped value, so it is encoded exactly as that value, and
MarshalYAMLandMarshalTextmethods with a pointer receiver are honoured.option.SomeInt(0)is0, notnull.
The omitempty YAML tag option drops absent values: yaml.v3 consults
IsZero(), which reports absence.
yaml.v3 never calls UnmarshalYAML for a null node (null, ~ or an
empty value) and leaves the target unchanged. Decoding null into a zero
optional (a fresh variable, a field of a fresh struct) yields None, but
decoding it into an optional that already holds a value keeps the value.
type User struct {
Name string `yaml:"name"`
Phone option.String `yaml:"phone"`
Email option.String `yaml:"email,omitempty"`
}
data, _ := yaml.Marshal(User{Name: "Maryamu Efe"})
// name: Maryamu Efe
// phone: nullA Go code generator for creating optional types with MessagePack serialization support.
Gentype generates wrapper types for various Go primitives and custom types that implement optional (some/none) semantics with full MessagePack serialization capabilities. These generated types are useful for representing values that may or may not be present, while ensuring proper encoding and decoding when using MessagePack.
- Generates optional types for built-in types (bool, int, float, string, etc.)
- Supports custom types with MessagePack extension serialization
- Provides common optional type operations:
SomeXxx(value)- Create an optional with a valueNoneXxx()- Create an empty optionalUnwrap(),UnwrapOr(),UnwrapOrElse()- Value extractionIsSome(),IsNil()- Presence checking
- Full MessagePack
CustomEncoderandCustomDecoderimplementation - JSON
MarshalerandUnmarshalerimplementation (see Encoding to JSON) - YAML marshalling without a YAML dependency (see Encoding to YAML)
- Type-safe operations
go install github.com/tarantool/go-option/cmd/gentypes@latest
# OR (for go version 1.24+)
go get -tool github.com/tarantool/go-option/cmd/gentypes@latestTo generate optional types for existing types in a package:
gentypes -package ./path/to/package -ext-code 123
# OR (for go version 1.24+)
go tool gentypes -package ./path/to/package -ext-code 123Or you can use it to generate file from go:
//go:generate go run github.com/tarantool/go-option/cmd/gentypes@latest -ext-code 123
// OR (for go version 1.24+)
//go:generate go tool gentypes -ext-code 123Flags:
-package: Path to the Go package containing types to wrap (default:".")-ext-code: MessagePack extension code to use for custom types (must be between -128 and 127, no default value)-verbose: Enable verbose output (default:false)-force: Ignore absence of marshal/unmarshal methods on type (default:false). Helpful for types from third-party modules.-imports: Add imports to generated file (default is empty). Helpful for types from third-party modules.-marshal-func: func that should do marshaling (default isMarshalMsgpackmethod). Helpful for types from third-party modules. Should be func of typefunc(v T) ([]byte, error)and should be located in the same dir or should be imported.-unmarshal-func: func that should do unmarshalling (default isUnmarshalMsgpackmethod). Helpful for types from third-party modules. Should be func of typefunc(v *T, data []byte) errorand should be located in the same dir or should be imported.
Sometimes you need to generate an optional type for a type from a third-party module,
and you can't add MarshalMsgpack/UnmarshalMsgpack methods to it.
In this case, you can use the -force, -imports, -marshal-func, and -unmarshal-func flags.
For example, to generate an optional type for github.com/google/uuid.UUID:
-
Create a file with marshal and unmarshal functions for the third-party type. For example,
uuid.go:package main import ( "errors" "github.com/google/uuid" ) func encodeUUID(uuid uuid.UUID) ([]byte, error) { return uuid[:], nil } var ( ErrInvalidLength = errors.New("invalid length") ) func decodeUUID(uuid *uuid.UUID, data []byte) error { if len(data) != len(uuid) { return ErrInvalidLength } copy(uuid[:], data) return nil }
-
Use the following
go:generatecommand://go:generate go run github.com/tarantool/go-option/cmd/gentypes@latest -package . -imports "github.com/google/uuid" -type UUID -marshal-func "encodeUUID" -unmarshal-func "decodeUUID" -force -ext-code 100
Generated types provide methods for working with optional values and 2 constructors for every single type:
Some<TypeName> creates option with some value and None<TypeName> creates the empty one
( is the original type name started with upper case):
// Create an optional with a value.
opt := option.SomeString("hello")
// Check if a value is present.
if opt.IsSome() {
value := opt.Unwrap()
fmt.Println(value)
}
// Use a default value if none.
value := opt.UnwrapOr("default")
// Encode to MessagePack.
err := opt.EncodeMsgpack(encoder)You could use our Makefile targets:
make codespell
make test
make testrace
make coveralls-deps
make coveralls
make coverageTo run default set of tests directly:
go test ./... -count=1Along with the approach supplied with go-option library pointer-based and slice-based approaches were benchmarked as well.
# int
BenchmarkNoneInt/Typed-8 560566400 2.200 ns/op 0 B/op 0 allocs/op
BenchmarkNoneInt/Generic-8 543332625 2.193 ns/op 0 B/op 0 allocs/op
BenchmarkNoneInt/GenericPtr-8 487631254 2.474 ns/op 0 B/op 0 allocs/op
BenchmarkNoneInt/GenericSlice-8 441513422 2.608 ns/op 0 B/op 0 allocs/op
# string
BenchmarkNoneString/Typed-8 170894025 6.545 ns/op 0 B/op 0 allocs/op
BenchmarkNoneString/Generic-8 185572758 6.451 ns/op 0 B/op 0 allocs/op
BenchmarkNoneString/GenericPtr-8 159143874 7.459 ns/op 0 B/op 0 allocs/op
BenchmarkNoneString/GenericSlice-8 173419598 6.708 ns/op 0 B/op 0 allocs/op
# struct
BenchmarkNoneStruct/Typed-8 384845384 3.107 ns/op 0 B/op 0 allocs/op
BenchmarkNoneStruct/Generic-8 415633797 2.884 ns/op 0 B/op 0 allocs/op
BenchmarkNoneStruct/GenericPtr-8 331620082 3.580 ns/op 0 B/op 0 allocs/op
BenchmarkNoneStruct/GenericSlice-8 387593746 3.115 ns/op 0 B/op 0 allocs/op
# int
BenchmarkSomeInt/Typed-8 499550200 2.231 ns/op 0 B/op 0 allocs/op
BenchmarkSomeInt/Generic-8 321369986 3.491 ns/op 0 B/op 0 allocs/op
BenchmarkSomeInt/GenericPtr-8 64221356 16.03 ns/op 8 B/op 1 allocs/op
BenchmarkSomeInt/GenericSlice-8 71858188 16.53 ns/op 8 B/op 1 allocs/op
# string
BenchmarkSomeString/Typed-8 192472155 5.840 ns/op 0 B/op 0 allocs/op
BenchmarkSomeString/Generic-8 197161162 6.471 ns/op 0 B/op 0 allocs/op
BenchmarkSomeString/GenericPtr-8 16207524 98.67 ns/op 16 B/op 1 allocs/op
BenchmarkSomeString/GenericSlice-8 12426998 100.4 ns/op 16 B/op 1 allocs/op
# struct
BenchmarkSomeStruct/Typed-8 358631294 3.407 ns/op 0 B/op 0 allocs/op
BenchmarkSomeStruct/Generic-8 241312274 4.978 ns/op 0 B/op 0 allocs/op
BenchmarkSomeStruct/GenericPtr-8 32534370 33.28 ns/op 24 B/op 1 allocs/op
BenchmarkSomeStruct/GenericSlice-8 34119435 33.08 ns/op 24 B/op 1 allocs/op
At this point we can see already that the alternatives (based on pointer and slice) require allocations while the approach implemented in go-option doesn't.
Now let's check encoding and decoding.
# int
BenchmarkEncodeDecodeInt/Typed-8 46089481 22.66 ns/op 0 B/op 0 allocs/op
BenchmarkEncodeDecodeInt/Generic-8 10070619 119.6 ns/op 32 B/op 2 allocs/op
BenchmarkEncodeDecodeInt/GenericPtr-8 20202076 58.14 ns/op 16 B/op 2 allocs/op
BenchmarkEncodeDecodeInt/GenericSlice-8 17400481 66.24 ns/op 24 B/op 3 allocs/op
# string
BenchmarkEncodeDecodeString/Typed-8 6053182 191.4 ns/op 8 B/op 1 allocs/op
BenchmarkEncodeDecodeString/Generic-8 1891269 668.3 ns/op 56 B/op 3 allocs/op
BenchmarkEncodeDecodeString/GenericPtr-8 1645518 659.2 ns/op 56 B/op 4 allocs/op
BenchmarkEncodeDecodeString/GenericSlice-8 1464177 775.4 ns/op 72 B/op 5 allocs/op
# struct
BenchmarkEncodeDecodeStruct/Typed-8 12816339 90.85 ns/op 3 B/op 1 allocs/op
BenchmarkEncodeDecodeStruct/Generic-8 2304001 532.5 ns/op 67 B/op 3 allocs/op
BenchmarkEncodeDecodeStruct/GenericPtr-8 2071520 570.2 ns/op 75 B/op 4 allocs/op
BenchmarkEncodeDecodeStruct/GenericSlice-8 2007445 587.4 ns/op 99 B/op 5 allocs/op
As it can be seen generic implementation ~3-4 times slower than the typed one. Thus it is recommended to use pre-generated optionals for basic types supplied with go-option (option.Int, option.String etc.).
BSD 2-Clause License