いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-279 (Functional Option Patternのメモ)

概要

以下、自分用のメモです。そんなに使う場面は無いのですが、たまに使うときがあるので、ついでにここにメモメモ。。

Goには Functional Option Pattern という実装パターンがあって、Goにはオプショナルなパラメータが存在しないので擬似的に実現する方法ですね。

これが不便と思うことはそんなに無いのですが、たまーに必要になったりします。

考え方としては、パラメータとして「値を設定する関数」をもらうようにしておいて、生成時にそれをループさせて自身に適用させるという形です。

こんな感じ。

package main

import "fmt"

type (
    Me struct {
        V1 int
        V2 string
        V3 int
        V4 string
    }
)

func New(v1 int, v2 string, opts ...func(me *Me)) *Me {
    me := new(Me)

    me.V1 = v1
    me.V2 = v2
    me.V3 = -1
    me.V4 = "unknown"

    for _, opt := range opts {
        opt(me)
    }

    return me
}

func main() {
    m1 := New(1, "hello")
    fmt.Printf("%v\n", m1)

    m2 := New(2, "world", func(me *Me) { me.V4 = "golang" })
    fmt.Printf("%v\n", m2)
}
$ go run main.go
&{1 hello -1 unknown}
&{2 world -1 golang}

でも、これだけだと利用者側に毎回 func(me *Me) 作ってもらわないと駄目だし、間違える可能性も高いので予め各値を設定する関数を返す関数を定義しておいて、それを使ってもらいましょうってなります。

こんな感じ。

package main

import "fmt"

type (
    Me struct {
        V1 int
        V2 string
        V3 int
        V4 string
    }

    Option func(me *Me)
)

func WithV3(v int) Option {
    return func(me *Me) {
        me.V3 = v
    }
}

func WithV4(v string) Option {
    return func(me *Me) {
        me.V4 = v
    }
}

func New(v1 int, v2 string, opts ...Option) *Me {
    me := new(Me)

    me.V1 = v1
    me.V2 = v2
    me.V3 = -1
    me.V4 = "unknown"

    for _, opt := range opts {
        opt(me)
    }

    return me
}

func main() {
    m1 := New(1, "hello")
    fmt.Printf("%v\n", m1)

    m2 := New(2, "world", WithV4("golang"))
    fmt.Printf("%v\n", m2)
}

同じことをしていますが、Withなんちゃらってものが存在しているので、利用者側からはこれを使って好きな設定を渡してもらうことが出来ます。

少しフレンドリーになった感じ。公開する側からみると、手間が増えますが。

サンプル(1) シンプルな形

config.go

package config

import (
    "fmt"
    "time"
)

type (
    Config struct {
        Addr        string
        Port        int
        RecvTimeout time.Duration
        SendTimeout time.Duration
    }

    // Option Pattern を実現するために用意
    Option func(c *Config)
)

func New(addr string, port int, options ...Option) *Config {
    c := new(Config)

    c.Addr = addr
    c.Port = port

    // Option Pattern
    for _, opt := range options {
        opt(c)
    }

    return c
}

func (c *Config) String() string {
    return fmt.Sprintf(
        "addr=%v:%v\trecvTimeout=%v\tsendTimeout=%v",
        c.Addr,
        c.Port,
        c.RecvTimeout,
        c.SendTimeout)
}

// Option Pattern
func WithRecvTimeout(v time.Duration) Option {
    return func(c *Config) {
        c.RecvTimeout = v
    }
}

// Option Pattern
func WithSendTimeout(v time.Duration) Option {
    return func(c *Config) {
        c.SendTimeout = v
    }
}

main.go

// Option Pattern についてのサンプルです。
//
// #REFERENCES
//   - https://dev.to/c4r4x35/options-pattern-in-golang-10ph
package main

import (
    "fmt"
    "time"

    "github.com/devlights/try-golang/examples/singleapp/option_pattern/config"
)

func main() {
    err := run()
    if err != nil {
        panic(err)
    }
}

func run() error {
    var (
        c *config.Config
    )

    c = config.New(
        "172.16.0.111",
        8888,
        config.WithRecvTimeout(30*time.Second),
        config.WithSendTimeout(5*time.Second),
    )
    fmt.Println(c)

    c = config.New(
        "localhost",
        12345,
    )
    fmt.Println(c)

    return nil
}
$ task -d examples/singleapp/option_pattern/
task: [default] go run main.go
addr=172.16.0.111:8888  recvTimeout=30s sendTimeout=5s
addr=localhost:12345    recvTimeout=0s  sendTimeout=0s

サンプル(2) 非公開な型を使ってのやり方

Option型と非公開なoptions型を定義して、利用者側にOptionを設定するためには WithXXX を指定しないと無理なようにするやり方。

package main

import (
    "errors"
    "fmt"
    "time"
)

type (
    Server struct {
        Addr        string
        SendTimeout time.Duration
    }

    options struct {
        sendTimeout time.Duration
    }

    Option func(o *options) error
)

func NewServer(addr string, opts ...Option) (*Server, error) {
    var options options
    for _, opt := range opts {
        err := opt(&options)
        if err != nil {
            return nil, err
        }
    }

    s := new(Server)
    s.Addr = addr
    s.SendTimeout = options.sendTimeout

    return s, nil
}

func WithSendTimeout(v time.Duration) Option {
    return func(o *options) error {
        if v < 1*time.Second {
            return errors.New("value should be greater than 1")
        }
        o.sendTimeout = v
        return nil
    }
}

func main() {
    s1, err := NewServer(":8888")
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", s1)

    s2, err := NewServer(":8888", WithSendTimeout(0*time.Second))
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", s2)

    s3, err := NewServer(":8888", WithSendTimeout(3*time.Second))
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", s3)
}

参考情報

dev.to

blog.web-apps.tech

raahii.github.io

golang.cafe

Go言語による並行処理

Go言語による並行処理

Amazon


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。