struct, method, interfaceの活用

Goの特徴として「クラス構文が存在しない」「継承が存在しない」などが挙げられます。代わりに、構造体に処理(メソッド)を紐づけることができます。ここでは、構造体(struct)、method、interfaceの基本的な実装方法を確認します。

目次

struct|構造体

typeで型定義

type で新しく T型 を定義しています。

package main

import (
	"fmt"
)

type T struct {
	PublicField  int // 先頭大文字はpublic(外部packageから参照可能)
	privateField int // 先頭小文字はprivate(外部packageから参照不可)
}

func main() {
	t := T{PublicField: 100, privateField: 200}
	fmt.Printf("%+v\n", t)
}
{PublicField:100 privateField:200}

構造体のフィールドですが、頭文字を 大文字 にするか 小文字 にするかで、外部packageからのアクセス権が変わります。

構造体は値型 ( 関数呼び出し時に考慮 )

構造体は参照型ではなく値型です。そのため、関数を呼び出すとき値のコピーが行われて、元の構造体は影響を受けません。

もし、関数で元の構造体を更新したい場合、以下 func2 のようにポインタを受け取るように実装する必要があります。

package main

import (
	"fmt"
)

type T1 struct {
	Field1 int
	Field2 int
}

func func1(t T1) {
	t.Field1 = t.Field1 * 2
	t.Field2 = t.Field2 * 2
}

func func2(t *T1) {
	t.Field1 = t.Field1 * 2
	t.Field2 = t.Field2 * 2
}

func main() {
	t1 := T1{Field1: 100, Field2: 200}
	fmt.Printf("%+v\n", t1)

	fmt.Println("--------------")
	func1(t1)
	fmt.Printf("%+v\n", t1)

	fmt.Println("--------------")
	func2(&t1)
	fmt.Printf("%+v\n", t1)
}
{Field1:100 Field2:200}
--------------
{Field1:100 Field2:200}
--------------
{Field1:200 Field2:400}

コンストラクタ|New

goにコンストラクタは存在しません。

代わりに、NewNewXxx のような関数名でコンストラクタのような処理を実装する命名上の慣習があります。

package main

import "fmt"

type T4 struct {
	Field1 int
	Field2 int
}

func NewT4(x, y int) *T4 {
	return &T4{Field1: x, Field2: y}
}

func main() {
	t4 := NewT4(10, 20)
	fmt.Printf("%+v\n", t4)
	fmt.Println(t4.Field1)
	fmt.Println(t4.Field2)
}
&{Field1:10 Field2:20}
10
20

フィールドにタグ付け

構造体のフィールドには、タグ付けできます。

package main

import (
	"fmt"
	"reflect"
)

type T2 struct {
	Field1 int "aaa"
	Field2 int "bbb"
}

func main() {
	t2 := T2{Field1: 100, Field2: 200}

	t := reflect.TypeOf(t2)
	fmt.Println(t.Field(0))
	fmt.Println(t.Field(0).Tag)
	fmt.Println(t.Field(1))
	fmt.Println(t.Field(1).Tag)
}
{Field1  int aaa 0 [0] false}
aaa
{Field2  int bbb 8 [1] false}
bbb

gormなどで活用されています。

method

Goにはクラス構文が存在しません。代わりに、typeで定義した型に処理(method)を紐づけることができます。

public, private

method名の頭文字を 大文字 にするか 小文字 にするかで、外部packageからのアクセス権が変わります。

下記例では、Method1 は外部packageから利用できますが、method2 は外部packageから利用できません。

package main

import "fmt"

type T3 struct {
	Field1 int
	Field2 int
}

func (v T3) Method1() {
	fmt.Println(v.Field1)
}

func (v T3) method2() {
	fmt.Println(v.Field2)
}

func main() {
	t3 := T3{Field1: 100, Field2: 200}
	t3.Method1()
	t3.method2()
}
100
200

値レシーバ、ポインタレシーバ

以下のように 値レシーバ だとstructの内容を更新できません。

structの内容を更新したい場合、ポインタレシーバ を定義します。

package main

import (
	"errors"
	"fmt"
)

type T5 struct {
	Field1 int
	Field2 int
}

// Method1 値レシーバ
func (v T5) Method1(x int) {
	v.Field1 = x
}

// Method2 ポインタレシーバ
func (v *T5) Method2(x int) error {
	if v == nil {
		return errors.New("nil error")
	}

	v.Field1 = x
	return nil
}

func main() {
	t5 := T5{Field1: 100, Field2: 200}
	fmt.Printf("%+v\n", t5)

	t5.Method1(10000)
	fmt.Printf("%+v\n", t5)

	if err := t5.Method2(10000); err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%+v\n", t5)

	var t5nil *T5
	t5nil = nil
	if err := t5nil.Method2(10000); err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%+v\n", t5nil)
}
{Field1:100 Field2:200}
{Field1:100 Field2:200}
{Field1:10000 Field2:200}
nil error
<nil>

ポインタレシーバではnil の可能性があるため nil のチェックを行っています。もし nil のチェックを外した場合、nilの操作をする時点でpanicになります。

処理の再利用
( 埋め込み型 )

Goには、継承はありません。

処理を再利用したいときなどは、埋め込み型を活用できます。

下記例では T6型T6Base型 を埋め込んでいます。

package main

import "fmt"

type T6Base struct {
	Field1 int
	Field2 int
}

func (v T6Base) Method1() {
	fmt.Println("T6Base\tMethod1 Field1:", v.Field1, " Field2:", v.Field2)
}

type T6 struct {
	T6Base
	Field3 int
}

func (v T6) Method2() {
	fmt.Println("T6\tMethod2 Field1:", v.Field1, " Field2:", v.Field2, " Field3:", v.Field3)
}

func NewT6(x, y, z int) *T6 {
	return &T6{T6Base{x, y}, z}
}

func main() {
	t6 := NewT6(100, 200, 300)
	fmt.Printf("%+v\n", t6)
	fmt.Printf("%+v\n", t6.Field1)
	fmt.Printf("%+v\n", t6.Field2)
	fmt.Printf("%+v\n", t6.Field3)

	t6.Method1()
	t6.Method2()
}
&{T6Base:{Field1:100 Field2:200} Field3:300}
100
200
300
T6Base  Method1 Field1: 100  Field2: 200
T6      Method2 Field1: 100  Field2: 200  Field3: 300

ただし、埋め込み型を利用すると、以下のようなデメリットもあります。

  • T6がどういったメソッドを持っているか分かりづらい。
  • T6BaseでMethod1が削除されたときに破壊的変更が発生する。

面倒ですが、基本的には以下のようにWrapする形で同じメソッドを再定義したほうが良いと思います。

type T6 struct {
	base   *T6Base
	Field3 int
}

func (v T6) Method1() {
	v.base.Method1()
}

func (v T6) Method2() {
	fmt.Println("T6\tMethod2 Field1:", v.base.Field1, " Field2:", v.base.Field2, " Field3:", v.Field3)
}

func NewT6(x, y, z int) *T6 {
	return &T6{base: &T6Base{x, y}, Field3: z}
}

interface

interfaceでは、メソッド名だけを宣言します。

interfaceで宣言した同一メソッドを実装することで、interfaceを実装できます。implementsなどの宣言は不要です。

package main

import (
	"fmt"
)

type MyInterface interface {
	MethodAaa(x, y int) string
}

type MyS1 struct {
	Name string
}

func (s MyS1) MethodAaa(x, y int) string {
	sum := x + y
	return fmt.Sprintf("MyS1 Name: %+v sum: %v\n", s.Name, sum)
}

type MyS2 struct {
	Name string
}

func (s MyS2) MethodAaa(x, y int) string {
	sub := x - y
	return fmt.Sprintf("MyS2 Name: %+v sub: %v\n", s.Name, sub)
}

func main() {
	f := func(i MyInterface) {
		fmt.Print(i.MethodAaa(100, 50))
	}
	myS1 := MyS1{"hello"}
	myS2 := MyS2{"world"}

	f(myS1)
	f(myS2)
}
MyS1 Name: hello sum: 150
MyS2 Name: world sub: 50

参考

よかったらシェアしてね!
目次