Goで構造体へのインタフェース埋め込み活用例(aws-sdk-goの事例など)

PUBLISHED ON 2019-03-16 — AWS, AWS-SDK-GO, GO, GOLANG

Summary

構造体へのインタフェース埋め込み活用例 ×2

  1. 一部メソッドを別処理に差し替え
  2. 特定のメソッドだけを実装

    • 外部APIアクセスなどを伴う機能のテストをmockで行いたい場合に、gomockのようなmock用ライブラリに依存せずにmock化を実現

      type Intf interface {
      MethodY()
      MethodZ()
      }
      
      type B struct {
      Intf  // こういう埋め込み
      }

例1:一部メソッドを別処理に差し替え

無名インタフェースの埋め込み例として本家コードで参考になるのがsort/sort.go のこの実装 です。reverse構造体のコメントにも書いてある通りこうした無名インタフェースを埋め込んだ構造体を利用する事で「一部のメソッドだけ別の実装に差し替える」という事を実現しています。

単純なサンプルコードはこんな感じで、Intfというinterfaceを実装済のAという構造体を ReplaceMethodZ に渡すと、MethodZの挙動だけが異なる別の構造体を取得するという挙動になります。

(同じコードはこちら → https://play.golang.org/p/mh803JB5RSt

package main

import (
  "fmt"
)

type Intf interface {
  MethodY()
  MethodZ()
}

type A struct {
  Str string
}

func (a *A) MethodY() {
  fmt.Println(fmt.Sprintf("%s: MethodY", a.Str))
}

func (a *A) MethodZ() {
  fmt.Println(fmt.Sprintf("%s: MethodZ", a.Str))
}

type B struct {
  Intf
}

func (b *B) MethodZ() {
  fmt.Println("Replaced MethodZ")
}

func ReplaceMethodZ(org Intf) Intf {
  return &B{org}
}

func main() {
  a := &A{Str: "I am A"}
  a.MethodY() // print "I am A: MethodY"
  a.MethodZ() // print "I am A: MethodZ"

  r := ReplaceMethodZ(a)
  r.MethodY() // print "I am A: MethodY"
  r.MethodZ() // print "Replaced MethodZ" <= replaced
}

例2:特定のメソッドだけを実装

sort.goの例は「インタフェースの全メソッドを実装済である構造体をあるメソッドに渡し、渡されたメソッド内で一部メソッドの実装を差し替える(上書きする)」という例でしたが、 下記のような「そもそもインタフェースのメソッドが部分的にしか実装されていない」ようなコードも書くことが出来ます。

(同じコードはこちら → https://play.golang.org/p/8YvXVtw91Mc

package main

import "fmt"

type Intf interface {
  GetStrVal() string
  GetIntVal() int
}

// Intfを実装するHoge1構造体
// 2メソッド漏れなく実装する
type Hoge1 struct {
  StrVal string
  IntVal int
}

func (h *Hoge1) GetStrVal() string {
  return h.StrVal
}

func (h *Hoge1) GetIntVal() int {
  return h.IntVal
}

// 無名インタフェース Intf を埋め込んだHoge2構造体
// GetIntVal メソッドのみを実装する
type Hoge2 struct {
  Intf
}

func (h *Hoge2) GetIntVal() int {
  return 100
}

// interfaceの実装済チェック
// Hoge2はGetStrValメソッドを実装していないがコンパイルエラーにならない
var _ Intf = (*Hoge1)(nil)
var _ Intf = (*Hoge2)(nil)

func main() {
  doIntfMethod(new(Hoge1))
  doIntfMethod(new(Hoge2))
}

func doIntfMethod(intf Intf) {
  fmt.Println(intf.GetIntVal())

  // GetStrValメソッドはHoge2では実装されていないので、呼び出そうとするとpanicが発生する
  // fmt.Println(intf.GetStrVal())
}

活用例として、「テストコードでテスト用の構造体を用意し、インタフェースのメソッド全部ではなくmockしたいメソッドだけを実装したい」というケースを、gomockのような外部ライブラリに頼る事なく実現出来ます。

aws-sdk-go での例

SQSへのアクセスが発生するアプリケーションのテストを、アクセス部分だけmockして書く例がAWS Developer Blogで紹介されています。詳細はリンク先を読んでいただくとして、リンク先のSQSでの例以外にも、例えばEC2でもec2iface というパッケージが用意されています。このパッケージのEC2APIというインタフェースが公式の、

Package ec2iface provides an interface to enable mocking the Amazon Elastic Compute Cloud service client for testing your code.

という紹介コメントの通り、テスト時のmock化をサポートしてくれるインタフェースになります。

実際には下記のような書き方で「アクセスが発生する部分だけ、テスト時にmockする」事が出来ます。EC2のインスタンス一覧を取得する場合なら DescribeInstances メソッド をmockすると良いです。

// 本体のコード

// ec2ifaceのインタフェースを無名で埋め込んだクライアント用構造体
// DescribeInstances メソッド もこのインタフェースのメンバーとして定義されている
type Client struct {
  ec2iface.EC2API
}

// EC2のインスタンス一覧を取得するメソッドを先程の構造体に用意しておく
func (client *Client) GetInstances() ([]*ec2.Instance, error) {
  // AWSへのアクセスが発生するのはこの箇所
  is, err := client.EC2API.DescribeInstances(&ec2.DescribeInstancesInput{})

  if err != nil {
    return nil, fmt.Errorf("error: %v", err)
  }

  var res []*ec2.Instance
  for _, r := range is.Reservations {
    res = append(res, r.Instances...)
  }

  return res, nil
}

func NewClient() *Client {
  client := new(Client)
  sess := session.Must(session.NewSession(aws.NewConfig()))
  client.EC2API = ec2.New(sess)  // 本体コードでは素のEC2オブジェクトをセット

  return client
}
// テストコード

// ec2iface.EC2APIインタフェースを無名で埋め込んだ、mockを行う為の構造体
// この構造体に独自のDescribeInstancesメソッドを用意してmockする
type mock struct {
  ec2iface.EC2API
}

// DescribeInstances メソッドだけを、仮の内容でmock
func (m *mock) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
  out := &ec2.DescribeInstancesOutput{
    Reservations: []*ec2.Reservation{
      &ec2.Reservation{Instances: []*ec2.Instance{dummy}},
    },
  }

  return out, nil
}

var dummy = &ec2.Instance{
  InstanceId:       aws.String("i-xxxxxxxxxx"),
  PrivateIpAddress: aws.String("172.31.1.1"),
  InstanceType:     aws.String("t2.nano"),
  State: &ec2.InstanceState{
    Name: aws.String("running"),
  },
}

func TestGetInstances(t *testing.T) {
  client := &Client{EC2API: &mock{}}  // テスト用にmockオブジェクトをセット
  insts, err := client.GetInstances()  // mockしたDescribeInstancesメソッドで返しているインスタンス情報が取得される

  if err != nil {
    t.Errorf("error occured. err: %#v", err)
  }

  if insts[0].InstanceId != dummy.InstanceId {
    t.Errorf("expected: %#v, but actual: %#v", dummy.InstanceId, insts[0].InstanceId)
  }
}

ブックマーク