Optional is a library that provides option types for the primitive Go types.
It can also be used as a tool to generate option type wrappers around your own types.
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.
This repository is a fork of github.com/markphelps/optional, which also provides option types for Go primitives and custom types. The driving motivation for this fork is performance. Where the original project uses pointer indirection to implement option types, this project inlines values directly into their option wrapper. This inlining avoids memory allocations, reduces garbage collection pressure, and improves cache locality.
The library provides option types each primitive Go type:
- bool
- byte
- complex128
- complex64
- float32
- float64
- int
- int16
- int32
- int64
- int8
- rune
- string
- uint
- uint16
- uint32
- uint64
- uint8
- uintptr
- error
package main
import (
"fmt"
"github.com/nvanbenschoten/optional"
)
func main() {
s := optional.MakeString("foo")
if s.Present() {
fmt.Println(s.Get())
} else {
fmt.Println("missing")
}
if val, ok := s.GetOrBool(); ok {
fmt.Println(val)
} else {
fmt.Println("missing")
}
if val, err := s.GetOrErr(); err != nil {
fmt.Println(err)
} else {
fmt.Println(val)
}
t := optional.String{}
fmt.Println(t.GetOr("bar"))
}
See example_test.go and the documentation for more usage.
Each option type is composed of its value type and a bool
inlined into a single struct. For example, the option type for an int32
is represented as struct{int32, bool}
. The precise memory layout that this structure translates to is subject to Go's type alignment and padding rules, which make little in the way of guarantees around struct field alignment.
In practice, however, the memory layout for this struct looks like:
+---+---+---+---+---+---+---+---+
| i | i | i | i | b | p | p | p |
+---+---+---+---+---+---+---+---+
i = int32 byte
b = bool byte
p = padding byte
total size = 8 bytes
The following table lists the memory footprint in bytes of each of the option types provided by the library when compiled for a 64-bit CPU architecture using Go 1.13.9:
Type | Size (bytes) |
---|---|
Bool | 2 |
Byte | 2 |
Complex128 | 24 |
Complex64 | 12 |
Error | 24 |
Float32 | 8 |
Float64 | 16 |
Int | 16 |
Int16 | 4 |
Int32 | 8 |
Int64 | 16 |
Int8 | 2 |
Rune | 8 |
String | 24 |
Uint | 16 |
Uint16 | 4 |
Uint32 | 8 |
Uint64 | 16 |
Uint8 | 2 |
Uintptr | 16 |
These sizes will differ depending on target CPU architecture and may change in future compiler versions. Changes here will break the tests in size_test.go.
No effort has been made to specialize the implementation of specific option types in order to optimize for memory size. Future work may explore such optimizations in a manner akin to Rust's "niche-filling strategy".
The tool can generate option type wrappers around your own types.
go get -u github.com/nvanbenschoten/optional/cmd/optional
Typically this process would be run using go generate, like this:
//go:generate optional -type=Foo
running this command:
optional -type=Foo
in the same directory will create the file optional_foo.go containing a definition of:
type OptionalFoo struct {
...
}
The default type is OptionalT or optionalT (depending on if the type is exported) and output file is optional_t.go. This can be overridden with the -output flag.
As you can see test coverage is a bit lacking for the library. This is simply because testing generated code is not super easy. I'm currently working on improving test coverage for the generated types, but in the meantime checkout string_test.go and int_test.go for examples.
Also checkout:
- example_test.go for example usage.
- cmd/optional/golden_test.go for golden file based testing of the generator itself.
If changing the API you may need to update the golden files for your tests to pass by running:
go test ./cmd/optional/... -update
.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request