2025/05/26
はじめに
Goでは、「存在するかわからない」値を表現するにあたって、
- ゼロ値で表現
- 例えば
-1
などの使わない範囲を決め、それで表現 - 型をポインタ画にしてnilで表現
などの手法が使われることが多いと思われる。
しかし、これらの手法はそれぞれに問題があり、かつ、コード全体でその意図を意識しながら読み書きする必要が出てしまい、ハイコンテキストなコードとなってしまうおそれもある。
そこで、このようなシチュエーションでつかえるようなOption型を実装してみた。
コンセプト
目指すのはこのあたり。
- 型で意味を表現できるようにする
- メモリ効率を維持する
- メモリ安全性を向上する
使用例
パッケージはGitHubに公開している。 (2025/05/26現在)
package main
import (
"fmt"
"github.com/ysuzuki19/robustruct/pkg/option"
)
type User struct {
Name string // 必須フィールド
Age option.Option[int] // 存在するかわからないフィールド
}
func main() {
user := User{
Name: "Alice",
Age: option.NewSome(30), // 存在する場合
}
if user.Age.IsSome() {
fmt.Printf("User %s is %d years old.\n", user.Name, user.Age.Unwrap())
} else {
fmt.Printf("User %s age is unknown.\n", user.Name)
}
// 存在しない場合
user2 := User{
Name: "Bob",
Age: option.None[int](), // 存在しない場合、型指定とともに Noneを使う
}
if user2.Age.IsSome() {
fmt.Printf("User %s is %d years old.\n", user2.Name, user2.Age.Unwrap())
} else {
fmt.Printf("User %s age is unknown.\n", user2.Name)
}
}
Option型の実装
参考にするのはRustのOption型とし、用途やメソッドの種類などは参考にしつつ、型情報や内部の挙動はGoの慣例に合わせて実装する。
構造体そのものはシンプルで、単に T
型のポインタを保持する。このシンプルな仕組みにより、Optionによる抽象化によるメモリ使用量の増加を抑える。
type Option[T any] struct {
ptr *T
}
実装のポイント
構造体の定義について
考慮している点として、「Goではゼロ値で自動で初期化される」のを前提とする必要があり、この場合でも安全に動く必要がある。
つまり、例えば
type Option[T any] struct {
ptr *T,
is_some bool
}
のような構造体にして、データ管理と状態管理を別にしてしまうと、間違えて option.Option {is_some: true}
のように初期化してしまった場合に、意図せずnilポインタを参照してしまう危険性がある。
それを避けたい場合、o.is_some && o.ptr != nil
といった条件を書くことも可能だが、この比較を行う場合、実際には後半ブロックのみで十分であるため、この作りに落ち着いた。
unsafeな取得メソッド
safetyのみを考慮するならば、Option型のポインタを直接取得するメソッドは不要であるが、Goの慣例に合わせて、以下のようなメソッドを追加している。
Ptr() *T
- ポインタをそのまま取得する
これは、直接ポインタを取得するもので、それが存在するものなのかどうかは度外視し、単にポインタを取得する。
Get() (*T, bool)
- ポインタとその存在有無を取得する
map
アクセスなどでも見られるような、データとその存在有無を同時に取得するメソッド。
おわりに
こちらに公開しているので、よかったら使ってみてください!