Skip to content

Go メソッド

メソッドと関数の違いは、メソッドがレシーバーを持つことであり、関数は持ちません。また、カスタム型のみがメソッドを持つことができます。まず例を見てみましょう。

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

まず型 IntSlice を宣言します。これは []int をベース型としています。次に 3 つのメソッド GetSetLen を宣言します。メソッドの外見は関数とほとんど変わりませんが、(i IntSlice) という小さな部分が追加されています。i はレシーバーで、IntSlice はレシーバーの型です。レシーバーは他の言語の thisself に似ていますが、Go では明示的に指定する必要があります。

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

メソッドの使用は、クラスのメンバーメソッドを呼び出すのに似ています。まず宣言し、次に初期化し、最後に呼び出します。

値レシーバー

レシーバーには 2 つのタイプがあります。値レシーバーとポインタレシーバーです。まず例を見てみましょう。

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // 変更されましたが、何の影響もありません
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

上記のコードを実行すると、myInt の値は依然として 1 のままで、2 に変更されていません。メソッドが呼び出されると、レシーバーの値がメソッドに渡されます。上記の例のレシーバーは値レシーバーで、簡単に言えば仮パラメータと見なせます。仮パラメータの値を変更しても、メソッド外の値には何の影響もありません。では、ポインタを通じて呼び出すとどうなるでしょうか。

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

残念なことに、このようなコードでも内部の値を変更することはできません。Go はレシーバーの型にマッチさせるために、それを参照解除し、(*(&myInt)).Set(2) として解釈します。

ポインタレシーバー

少し修正するだけで、myInt の値を正常に変更できます。

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

現在のレシーバーはポインタレシーバーです。myInt は値型ですが、値型を通じてポインタレシーバーのメソッドを呼び出すと、Go はそれを (&myint).Set(2) として解釈します。したがって、メソッドのレシーバーがポインタの場合、呼び出し元がポインタかどうかに関係なく、内部の値を変更できます。

関数のパラメータ渡しの過程では、値コピーが行われます。整数を渡す場合はその整数をコピーし、スライスを渡す場合はそのスライスをコピーしますが、ポインタを渡す場合はそのポインタをコピーするだけで済みます。明らかに、ポインタを渡す方がスライスを渡すよりもリソースの消費が少なくなります。レシーバーも同様で、値レシーバーとポインタレシーバーも同じ道理です。ほとんどの場合、ポインタレシーバーを使用することを推奨しますが、両者を混在させるべきではありません。どちらか一方を使用するか、どちらも使用しないようにすべきです。以下の例をご覧ください。

TIP

事前に インターフェース を理解する必要があります。

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} 正しい方法
   an.Run()
}

このコードはコンパイルできません。コンパイラは以下のエラーを出力します。

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

翻訳すると、Dog{} を使用して Animal 型の変数を初期化できません。DogAnimal を実装していないためです。解決方法は 2 つあります。1 つ目はポインタレシーバーを値レシーバーに変更すること、2 つ目は Dog{}&Dog{} に変更することです。次にそれぞれを解説します。

go
type Dog struct {
}

func (d Dog) Run() { // 値レシーバーに変更
   fmt.Println("Run")
}

func main() { // 正常に実行可能
   var an Animal
   an = Dog{}
   // an = &Dog{} これも可能
   an.Run()
}

元のコードでは、Run メソッドのレシーバーは *Dog です。そのため、Animal インターフェースを実装するのは Dog 構造体ではなく Dog ポインタです。これらは 2 つの異なる型であるため、コンパイラは Dog{}Animal の実装ではないと判断し、変数 an に代入できません。したがって、2 つ目の解決方法は Dog ポインタを変数 an に代入することです。ただし、値レシーバーを使用する場合でも、Dog ポインタは依然として正常に animal に代入できます。これは Go が適切な状況でポインタを参照解除するためです。ポインタを通じて Dog 構造体を見つけることができるためです。しかし、その逆、つまり Dog 構造体を通じて Dog ポインタを見つけることはできません。構造体で値レシーバーとポインタレシーバーを単純に混在させるだけなら問題ありませんが、インターフェースと一緒に使用するとエラーが発生します。むしろ、いつでも値レシーバーのみを使用するか、ポインタレシーバーのみを使用するようにして、良好な規範を形成し、後続のメンテナンスの負担を減らすべきです。

もう 1 つの状況として、値レシーバーがアドレス指定可能な場合、Go は自動的にポインタ演算子を挿入して呼び出します。例えばスライスはアドレス指定可能で、値レシーバーを通じて内部の値を変更できます。以下のコードをご覧ください。

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

出力

[1]

ただし、これにより別の問題が発生します。要素を追加する場合、状況が異なります。以下の例をご覧ください。

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

出力は以前と同じです。append 関数は戻り値を持ち、スライスに要素を追加した後は元のスライスを上書きする必要があります。特に容量拡張後、メソッド内で値レシーバーを変更しても何の影響もありません。これが例の結果の原因です。ポインタレシーバーに変更すると正常になります。

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

出力

[1 2]

Golang学习网由www.golangdev.cn整理维护