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にコンストラクタは存在しません。
代わりに、New
や NewXxx
のような関数名でコンストラクタのような処理を実装する命名上の慣習があります。
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