2023/08/20

golang: interfaceの勉強 (3)

続き。

hiro99ma blog: golang: interfaceの勉強 (2)
https://blog.hirokuma.work/2023/08/golang-interface-2.html


struct と duck typing

golang の調べ物をしたとき、duck typing という言葉を見ることがある。
メソッド名・引数・戻り値が一致したら同じものと思われる、くらいに思っていたのでこういうことができると考えていた。

package main

import "fmt"

type Abc struct{}

func (a *Abc) Greet() {
    fmt.Println("Hello")
}

type Def struct{}

func (d *Def) Greet() {
    fmt.Println("Aloha")
}

func main() {
    abc := new(Abc)
    abc.Greet()
    abc = new(Def)
    abc.Greet()
}

これは赤文字のところでコンパイルエラーになる。コンパイルエラーというか文法エラーというか。

interface Ghi を作ってそこに Greet() を書いておき、使う場合は Ghi の変数として扱ってやると、Abc と Def が Ghi の派生であるとかを匂わせる必要もなく動くようになる。

package main

import "fmt"

type Ghi interface {
    Greet()
}

type Abc struct{}

func (a *Abc) Greet() {
    fmt.Println("Hello")
}

type Def struct{}

func (d *Def) Greet() {
    fmt.Println("Aloha")
}

func main() {
    var abc Ghi
    abc = new(Abc)
    abc.Greet()
    abc = new(Def)
    abc.Greet()
}

struct と interface を関連付けるような記述をしないというところが「プログラミング言語Go」のインターフェースの章で『暗黙的に満足される』といっている意味なんだろう。

暗黙的だと関連性がわからなくて悩みそうだと思ったなら、明示的に書いてもよいだろう。

type Abc struct{ Ghi }

明示的に interface があることを書いておきながらも、interface の方に定義されたメソッドを書いていない場合はコンパイルエラーになりそうだが、そうはならない。
どうなるかというと、未定義のメソッドを呼び出そうとした時点で実行時エラーになる。

panic: runtime error: invalid memory address or nil pointer dereference

package main

import "fmt"

type Ghi interface {
    Greet()
}

type Abc struct{ Ghi }

// func (a *Abc) Greet() {
//     fmt.Println("Hello")
// }

type Def struct{ Ghi }

func (d *Def) Greet() {
    fmt.Println("Aloha")
}

func main() {
    var abc Ghi
    abc = new(Abc)
    abc.Greet()
    abc = new(Def)
    abc.Greet()
}

goalng は全体的に縛り付けるような決まりが少ない気がする。その辺は C言語っぽいなーと感じる。

ともかく、golang は duck typing な言語なのかもしれないが(厳密な定義はよく知らない)、それは何でもかんでもじゃなくて interface が関係した場合だけなんだと思われる。


テストでinterfaceを使おうとする

mock なのか stub なのか driver なのか厳密なところ避けさせてもらう。
本物ではなく偽物にしたいということで fake という言葉を使おう。

テストで fake を使いたい場合、テストしたい関数なりメソッドなりは変更したくないのだが、その中で実際に行う処理は偽物に置き換えたいということだと思う。
ネットワークにアクセスする処理だがエラーを起こしたことにしてテストしたい、とか、ハードウェアにアクセスする処理だが実機では確認できないのでダミーの処理で置き換えたい、とか。

image

まず、テスト対象の関数/メソッドは、その中から呼び出す次の処理を切替ができるように書いておかないといかん。上の図だと黄色の丸で表しているが、これが golang でいうところの関数だったら切り替えるのは難しいだろう(タグで切り替えるということはできそうだが)。

ではメソッドの中から別のメソッドを呼ぶように書くとして、何をどう書くと良いだろう。

Golangにおけるinterfaceをつかったテストで mock を書く技法 - haya14busa
http://haya14busa.com/golang-how-to-write-mock-of-interface-for-testing/

こちらでは、対象となる方は interface を名前を付けて書いている。テスト対象自体が interface を所持しているのではなく外側にあって(所持してても良いだろうが)、new するときに設定しているのか。そして fake の方では interface を無名の埋め込みにしている。

 

ではもうちょっと簡易なコードにして試そう。
mocktest というディレクトリを作り、その中に main.go,  greet/greet.go, greet/machine.go というファイルを置く。

  • main.go
package main

import (
    "fmt"
    "mocktest/greet"
)

func main() {
    g := greet.NewGreet()
    g.SetData(3)
    fmt.Println(g.GetData())
}
  • greet/greet.go
package greet

type Greet struct {
    m     MachineInterface
    value int32
}

func NewGreet() *Greet {
    g := &Greet{}
    g.m = NewMachineData()
    return g
}

func (g *Greet) GetData() int32 {
    return g.value + g.m.Get()
}

func (g *Greet) SetData(value int32) {
    g.value = value
    g.m.Set(value)
}
  • greet/machine.go
package greet

import (
    "math/rand"
    "time"
)

type MachineInterface interface {
    Get() int32
    Set(value int32)
}

type MachineData struct {
    value int32

    r *rand.Rand
}

func NewMachineData() *MachineData {
    m := &MachineData{}
    m.r = rand.New(rand.NewSource(time.Now().UnixNano()))
    return m
}

func (m *MachineData) Get() int32 {
    multi := m.r.Int31n(100)
    return multi * m.value
}

func (m *MachineData) Set(value int32) {
    m.value = value
}

「go run .」などとして実行すると、毎回異なる値が出力されるだろう。
greet/machine.go が乱数を使っていて、ちょっとそのままではテストしづらいというようにしている。やっている内容として意味は無い。 greet/machine.go が制御できない動作をするということだけわかっていればよい。
ここでテストするならば greet/greet.go のメソッドになるし、fake にしたいのは greet/machine.go になる。

では適当にテストコードを書いてみる。

  • greet/greet_test.go
package greet

import (
    "testing"
)

type fakeMachineData struct {
    MachineInterface

    FakeGetData func() int32
    FakeSetData func(value int32)
}

func (f *fakeMachineData) Get() int32 {
    return f.FakeGetData()
}

func (f *fakeMachineData) Set(value int32) {
    f.FakeSetData(value)
}

func TestGetData1(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 10
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(3)
    value := mo.GetData()
    if value != 13 {
        t.Errorf("GetData: %v", value)
    }
}

func TestGetData2(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 15
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(5)
    value := mo.GetData()
    if value != 20 {
        t.Errorf("GetData: %v", value)
    }
}

$ go test ./greet/ -v
=== RUN   TestGetData1
--- PASS: TestGetData1 (0.00s)
=== RUN   TestGetData2
--- PASS: TestGetData2 (0.00s)
PASS
ok      mocktest/greet  0.001s

greet/greet_test.go は package が greet なので、小文字のフィールドにもアクセスできる。それを利用して greet.NewGreet() で設定している NewMachineData() を上書きしている。

 

greet/machine.go を別パッケージにしてみよう。
外部のライブラリを使っているとそうなるだろう。

  • greet/greet.go
package greet

import "mocktest/machine"

type Greet struct {
    m     machine.MachineInterface
    value int32
}

func NewGreet() *Greet {
    g := &Greet{}
    g.m = machine.NewMachineData()
    return g
}

func (g *Greet) GetData() int32 {
    return g.value + g.m.Get()
}

func (g *Greet) SetData(value int32) {
    g.value = value
    g.m.Set(value)
}
  • machine/machine.go
package machine

import (
    "math/rand"
    "time"
)

type MachineInterface interface {
    Get() int32
    Set(value int32)
}

type MachineData struct {
    value int32

    r *rand.Rand
}

func NewMachineData() *MachineData {
    m := &MachineData{}
    m.r = rand.New(rand.NewSource(time.Now().UnixNano()))
    return m
}

func (m *MachineData) Get() int32 {
    multi := m.r.Int31n(100)
    return multi * m.value
}

func (m *MachineData) Set(value int32) {
    m.value = value
}
  • greet/greet_test.go
package greet

import (
    "mocktest/machine"
    "testing"
)

type fakeMachineData struct {
    machine.MachineInterface

    FakeGetData func() int32
    FakeSetData func(value int32)
}

func (f *fakeMachineData) Get() int32 {
    return f.FakeGetData()
}

func (f *fakeMachineData) Set(value int32) {
    f.FakeSetData(value)
}

func TestGetData1(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 10
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(3)
    value := mo.GetData()
    if value != 13 {
        t.Errorf("GetData: %v", value)
    }
}

func TestGetData2(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 15
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(5)
    value := mo.GetData()
    if value != 20 {
        t.Errorf("GetData: %v", value)
    }
}

machine を import するようになるくらいの変更しかない。
外部のパッケージだったら interface があっても不自然ではない。

もしここで machine/machine.go に interface がなかったらどうしよう?
その場合は greet/greet.go の方で作っておけば良い。「暗黙的に満足される」から machine/machine.go は変更しなくても良いのだ。
案外、なんとかなるものだ。

  • macihne/machine.go
package machine

import (
    "math/rand"
    "time"
)

// type MachineInterface interface {
//     Get() int32
//     Set(value int32)
// }

type MachineData struct {
    value int32

    r *rand.Rand
}

func NewMachineData() *MachineData {
    m := &MachineData{}
    m.r = rand.New(rand.NewSource(time.Now().UnixNano()))
    return m
}

func (m *MachineData) Get() int32 {
    multi := m.r.Int31n(100)
    return multi * m.value
}

func (m *MachineData) Set(value int32) {
    m.value = value
}
  • greet/greet.go
package greet

import "mocktest/machine"

type MachineInterface interface {
    Get() int32
    Set(value int32)
}

type Greet struct {
    m     MachineInterface
    value int32
}

func NewGreet() *Greet {
    g := &Greet{}
    g.m = machine.NewMachineData()
    return g
}

func (g *Greet) GetData() int32 {
    return g.value + g.m.Get()
}

func (g *Greet) SetData(value int32) {
    g.value = value
    g.m.Set(value)
}
  • greet/greet_test.go
package greet

import (
    "mocktest/machine"
    "testing"
)

type fakeMachineData struct {
    machine.MachineData

    FakeGetData func() int32
    FakeSetData func(value int32)
}

func (f *fakeMachineData) Get() int32 {
    return f.FakeGetData()
}

func (f *fakeMachineData) Set(value int32) {
    f.FakeSetData(value)
}

func TestGetData1(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 10
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(3)
    value := mo.GetData()
    if value != 13 {
        t.Errorf("GetData: %v", value)
    }
}

func TestGetData2(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 15
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(5)
    value := mo.GetData()
    if value != 20 {
        t.Errorf("GetData: %v", value)
    }
}

2023/08/19

golang: interfaceの勉強 (2)

使ったことがない interface だが、テストでモックを作ろうとするときはやっておいた方がよさそうだ。

hiro99ma blog: [golang] interfaceの勉強
https://blog.hirokuma.work/2022/02/golang-interface.html


struct

golang には class はないが、struct で似たようなことはできる。
こういう意味が無いこともできる。

type Abc struct {}

func (a *Abc) Greet() {
  fmt.Println("Hello")
}

ここの「(a *Abc)」の a はレシーバーと呼ばれる。 class の this なんかに近いし、もし Abc の定義に変数があったなら a.xxx のようにしてアクセスできる。

そして Greet() はメソッドである。おそらくだが、レシーバーがない func は関数と呼んで、レシーバーがある func はメソッドと呼ぶんじゃないだろうか。まあ、C++とかでもそういう使い分けだったと思うし、一般的な呼び方をしておいてよさそうだ。
ちなみに構造体のメンバーは「フィールド」と呼ぶ。

この Greet() はAbc を参照しているわけではないし、そもそも Abc はフィールドを持つわけではないので関数であっても困らない。空の構造体はどういうシーンで使うんだろうか?
思いつくのはチャネルの待ち合わせだ。

hiro99ma blog: [golang] chan で待つ
https://blog.hirokuma.work/2022/06/golang-chan.html

この記事では type を使わず直接 struct {} を使っているが、待ち合わせるチャネルが複数ある場合はそれぞれに type で名前を付けることになるだろう。

それ以外だと・・・特に共通で使うような変数がないけど機能としてまとめておいた方がわかりやすいがパッケージにするほどでもないようなときにグループ化する目的とか?

 

struct は既に定義してある struct を中に組み込むことができる。
このとき、その構造体にフィールド名を付けることもできるし、付けないこともできる。付けた場合は他のフィールドと同じようにドットを使って名前を使って参照する。名前を付けなかった場合はあたかも組み込んだ struct のフィールドがそこに展開されているかのように扱うことができる。

なので、こういう組み込みにした場合は軽い継承関係のようなものができているのかと思っていたのだが、キャストのようなことをして代入することはできないそうだ。unsafe とか使ってやってみようとしたのだが、どうにもダメだった。

 

そう、ダメだったのだ。
だからモックにしたい struct を組み込んだ struct を作ってテストしよう、というのはできないのだ。
そういう場合は interface を使うようにするそうである。

・・・というつもりで書こうとしていたのだが、「モックのためだけに interface を使うのってどうなのよ」みたいな記事も見つけてしまい、ちょっと揺らいでいる。

テストのためだけに`interface`を書きたくないでござる — KaoriYa
https://www.kaoriya.net/blog/2020/01/20/never-interface-only-for-tests/

ただまあ、めんどうになるくらいだったら interface を作ってもいいかなというくらいの気持ちだ。C言語でヘッダファイルを書くような感じで。

ちなみに↑の記事の方は、同じ struct 名を用意し、テストの時と本番用でビルドタグを切り替えて読み替える、というようなことをやっている。ビルドタグという機能を知らなかったのだが、ファイル単位で指定できるのは便利そうだ。

 

長くなったので interface については次回。

2023/08/05

"docker builder prune"で/var/lib/docker/の削減をした

私は Widows11 上で VirtualBox を動かし、ゲストOS で Ubuntu を動かしている。
クラウド上で動かすときは 30GB でなんとかなっているからとのんびりしていたのだが、あれよあれよと容量が膨らんで / と /home をそれぞれ 180GB くらい確保する羽目になってしまった。

/home は自分でファイルを置いているから増えたり減ったりも制御しやすいのだが、 / の方はアプリのインストールとかやっているとじわじわ圧迫されてしまう。
さっき見ると空き容量が 2% くらいになっているのに気付いた。

Ubuntu にインストールされていた Disk Usage Analyzer で見てみたが、そこに出ている範囲では大して使われていない。ということはあまり触りたくないディレクトリがふくれているようだ。
du で見ていくと /var 、中でも /var/lib/docker が大半の容量を占めていることに気付く。

/var/lib/docker/overlay2の容量が大きいので削減したい - 日々精進
https://anton0825.hatenablog.com/entry/2022/08/11/000000

あー、docker か。
しばしば docker rmi でイメージをまるまる削除しているので安心していたのだが、それでいいというわけではないのか。

$ docker builder prune

これで Y とこたえるとずらーっと削除してくれた。135GB くらい削除してくれたようだ。
やれやれ。

 

no space は忘れた頃にやってくるので、次もたぶん忘れてるんだろうな。

win11 : 画像ファイルの関連付けが変更できなくなっていた

以前もなっていたが、また Windows11 で画像ファイルの関連付けが変更できなくなっていた。
どうやって前回直したか思い出せなかったが、こういう方法だった。

【フォト】画像ファイルの関連付けが出来ない問題の解決【jpg png bmp gif】 | rimo-WorkShop
https://rimo-ws.com/6860/

そうそう、Microsoftフォトを修復だかリセットだかやって使えるようになったのだ。

 

ただ今回違ったのは、私がMicrosoftフォトをアンインストールしていたことだった。
では何のアプリが起動していたかというと「画像の印刷」というアプリだった。
ちなみに「フォト レガシ」もインストールされていたのだが、そっちには関連付けられなかった。

 

結局、フォトをインストールして修復&リセットすると関連付けが変更できるようになった。
やれやれ。
Open-Shellを使っているせいかフォトがアンインストールできてしまったことがややこしくした原因かもしれんね。