読者です 読者をやめる 読者になる 読者になる

at kaneshin

Free space for me.

Go言語で構造体の関数を動的に変更する

Go言語の構造体に関数を定義したあとに、動的に処理を変更することはreflectを使わない限りできないです。 ただ、スタブやモックとして関数の挿げ替えを行いたい場合や、実装を動的に変更したいという特質な要件を持つ人もたまにはいるでしょう。

そんなときはC言語っぽい考えで実装をしてしまいましょう。

TL;DR

type Foo struct {
    stringFunc func(Foo) string
}

func (f Foo) String() string {
    if f.stringFunc == nil {
        return ""
    }
    return f.stringFunc(f)
}

The Go Playground

構造体のフィールドに関数

C言語の復讐

C言語オブジェクト指向っぽいものが流行っていた時は下記のように書くことがあったかと思います。

#include <stdio.h>
#include <stdlib.h>

struct foo {
    char *name;
    void (*func)(struct foo*);
};

void foo_func(struct foo* f)
{
    printf("@%s\n", f->name);
}

int main(int argc, char* argv[])
{
    struct foo* f = (struct foo*)malloc(sizeof(struct foo));
    f->name = "kaneshin";
    f->func = foo_func;
    f->func(f);
    return 0;
}

関数のポインタ変数を構造体のメンバーに定義しておき、そこに対してApplyするような実装になっています。

Go言語で実装

このC言語の特徴をGo言語でもうまく使ってしまおうということです。(関数オーバーヘッド?なにそれおいしいの?)

package main

import "fmt"

type User struct {
    name     string
    stringer func(User) string
}

func (u User) String() string {
    return u.stringer(u)
}

Userという構造体のフィールドにstringerという関数を作っています。引数に自身を渡すのはC言語の方を理解している人なら当たり前にわかると思いますが、呼び出し先ではどのような構造体の値からの呼び出しなのかが判断つかないので、呼び出し時にセットします。 これはGo言語でも同じなので気をつけてください。

func printer(f fmt.Stringer) {
    fmt.Printf("Stringer: %s\n", f.String())
}

func main() {
    u := User{name: "kaneshin"}

    // @<user.name>
    u.stringer = func(u User) string {
        return fmt.Sprintf("@%s:", u.name)
    }
    printer(u)

    // Hello <user.name>
    u.stringer = func(u User) string {
        return fmt.Sprintf("Hello %s", u.name)
    }
    printer(u)

}

Goの方ではfmtパッケージのStringer型 (interface) を満たすようしています。 上記のmainの中身を見てもらうとわかるかと思いますが、String()の中で呼ばれているstringerをここで設定しています。

おわりに

こういう実装をしていくと、定義が別の関数内に入ってしまったりするので、すべてのコードがテスタブルなコードを書くようにするにはまた一工夫必要であったりします。 また、関数が設定されていない場合はBAD ACCESSになるので気をつけてください。

この実装方法に慣れてしまって動的に変更したくなったとしても、用法・用量は守りましょう。

f:id:laplus-knsn:20160601011624p:plain