LoginSignup
16
8

More than 1 year has passed since last update.

Go言語:foo(s []int)とfoo(s *[]int)本質的な違い

Last updated at Posted at 2021-08-12

背景

業務上でgolangを使っています。スライスは関数の引数として使う場合は、ポインタを使うべきかどうかを説明いたします。

func foo(arr *[]int) { <- スライスのポインタを使う場合
   ***
}


func bar(arr []int) { <- スライスのポインタを使わない場合
  ***
}

結論

結論 1. 関数内からスライスが参照するArrayの要素だけ変更したい場合は、スライスのポインタを使う必要がありません

結論 2. 関数内から外側のスライスの変更(start,len,cap)スライスのポインタを使うべきです

結論 3. 関数内の変更がスライスが参照するArrayを作り直された場合、スライスポインタを使うべきです

説明

結論1. 関数内からスライスの要素だけを変更したい場合は、ポインタを使う必要がありません。 

package main

import "fmt"

func main()  {
    arr := []int{1,2,3}
    fmt.Println(fmt.Sprintf("スライス参照のArrayのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("変更前:%v",arr))

    fmt.Println(fmt.Sprintf("関数に渡す前スライスのメモリアドレス:%p", &arr))
    foo(arr)
    fmt.Println(fmt.Sprintf("変更後:%v",arr))
}


func foo(arr []int) {
    fmt.Println(fmt.Sprintf("関数に渡されたスライスのメモリアドレス:%p", &arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスが参照するArrayのメモリアドレス:%p", arr))
    arr[1] = 13
}
1. スライス参照のArrayのメモリアドレス:0xc000016100
2. 変更前:[1 2 3]
3. 関数に渡す前スライスのメモリアドレス:0xc00000c060
4. 関数に渡されたスライスのメモリアドレス:0xc00000c0c0 
5. 関数に渡されたスライスが参照するArrayのメモリアドレス:0xc000016100
6. 変更後:[1 13 3]

この例では、fooに渡したのはスライスのポインタではなく、スライスそのものです。
golangでは、関数に渡し方は値渡し方ですので、渡されたスライスがコピーされ、関数内に渡します。
3行目と4行目のメモリアドレスは異なったのは、その証拠です。

関数外と関数内のスライスはそれぞれ異なるスライスですが、同じ参照元を参照しています。
1行目と5行目のメモリアドレスは同じなのは、その証拠です。

そのため、fooを実行したら、外側のスライスが参照しているArrayも変わります

go slice parameter.002.jpeg

もちろん、以下のように引数をポインタ型として関数に渡してもよいですが、わざわざそうする必要がないです。

package main

import "fmt"

func main()  {
    arr := []int{1,2,3}
    fmt.Println(fmt.Sprintf("スライス参照のArrayのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("変更前:%v",arr))

    arrPointer := &arr
    fmt.Println(fmt.Sprintf("関数に渡す前スライスのメモリアドレス:%p", arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡す前にスライスポインタのメモリアドレス:%p", &arrPointer))
    foo(arrPointer)
    fmt.Println(fmt.Sprintf("変更後:%v",arr))
}


func foo(arrPointer *[]int) {
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタのメモリアドレス:%p", &arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスのメモリアドレス:%p", arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスから参照するArrayのメモリアドレス:%p", *arrPointer))
    (*arrPointer)[1] = 13
}
1. スライス参照のArrayのメモリアドレス:0xc000016100
2. 変更前:[1 2 3]
3. 関数に渡す前スライスのメモリアドレス:0xc00000c060
4. 関数に渡す前にスライスポインタのメモリアドレス:0xc00000e030
5. 関数に渡されたスライスポインタのメモリアドレス:0xc00000e038
6. 関数に渡されたスライスポインタが参照するスライスのメモリアドレス:0xc00000c060
7. 関数に渡されたスライスポインタが参照するスライスから参照するArrayのメモリアドレス::0xc000016100
8. 変更後:[1 13 3]

今回関数fooに渡したのはスライス実体ではなく、スライスのポインタです。
golangでは、関数に渡し方は値渡し方ですので、渡されたスライスポインタがコピーされて、関数内に渡します。
4と5のメモリアドレスは異なることはその証拠です

go slice parameter.002.jpeg

結論 2. 関数内から外側のスライス(start,len,cap)もしく外側スライス参照Arrayを変更したい場合は、ポインタを使うべきです

package main

import "fmt"

func main()  {
    arr := []int{1,2,3}
    fmt.Println(fmt.Sprintf("スライス参照のArrayのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("変更前:%v",arr))
    fmt.Println(fmt.Sprintf("関数に渡す前スライスのメモリアドレス:%p", &arr))
    foo(arr)
    fmt.Println(fmt.Sprintf("変更後:%v",arr))
}


func foo(arr []int) {
    fmt.Println(fmt.Sprintf("関数に渡されたスライスのメモリアドレス:%p", &arr))
    arr = arr[:2]
    fmt.Println(fmt.Sprintf("関数に渡されたスライスがスライスングされてから:%v",arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスがスライスングされてからのメモリアドレス:%p", &arr))
}

output

スライス参照のArrayのメモリアドレス:0xc000118000
変更前:[1 2 3]
関数に渡す前スライスのメモリアドレス:0xc000116000
関数に渡されたスライスのメモリアドレス:0xc000116060
関数に渡されたスライスがスライスングされてから:[1 2]
関数に渡されたスライスがスライスングされてからのメモリアドレス:0xc000116060
変更後:[1 2 3]

上記のように関数内では、渡されたスライスarrをスライシングして、lenを短くしましたが、外側のスライスarrには影響しないです。
go slice parameter.003.jpeg

この場合は、関数にスライスのポインタを渡したら、関数内のスライシング効果を外側のスライスに及ぼします。


package main

import "fmt"

func main()  {
    arr := []int{1,2,3}
    fmt.Println(fmt.Sprintf("スライス参照のArrayのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("変更前:%v",arr))
    fmt.Println(fmt.Sprintf("関数に渡す前スライスのメモリアドレス:%p", &arr))
    arrPointer := &arr
    fmt.Println(fmt.Sprintf("関数に渡す前スライスポインタのメモリアドレス:%p", &arrPointer))
    foo(arrPointer)
    fmt.Println(fmt.Sprintf("変更後:%v",arr))
}


func foo(arr *[]int) { // ポインタを引数に渡します
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタのメモリアドレス:%p", &arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスのメモリアドレス:%p", arr))
    *arr = (*arr)[:2]
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスがスライスングされてからのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスがスライスングされてから参照するArrayのメモリアドレス:%p", *arr))
}

スライス参照のArrayのメモリアドレス:0xc0000b4000
変更前:[1 2 3]
関数に渡す前スライスのメモリアドレス:0xc0000a6020
関数に渡す前スライスポインタのメモリアドレス:0xc0000ac020
関数に渡されたスライスポインタのメモリアドレス:0xc0000ac028
関数に渡されたスライスポインタが参照するスライスのメモリアドレス:0xc0000a6020
関数に渡されたスライスポインタが参照するスライスがスライスングされてからのメモリアドレス:0xc0000a6020
関数に渡されたスライスポインタが参照するスライスがスライスングされてから参照するArrayのメモリアドレス:0xc0000b4000
変更後:[1 2]

go slice parameter.004.jpeg

結論 3. 関数内の変更がスライスが参照するArrayを作り直された場合、スライスポインタを使うべきです

package main

import "fmt"

func main()  {
    arr := []int{1,2,3}
    fmt.Println(fmt.Sprintf("スライス参照のArrayのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("変更前:%v",arr))
    fmt.Println(fmt.Sprintf("関数に渡す前スライスのメモリアドレス:%p", &arr))
    foo(arr)
    fmt.Println(fmt.Sprintf("変更後:%v",arr))
}


func foo(arr []int) {
    fmt.Println(fmt.Sprintf("関数に渡されたスライスのメモリアドレス:%p", &arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスが参照するArrayのメモリアドレス:%p", arr))
    arr = append(arr,4)
    fmt.Println(fmt.Sprintf("関数に渡されたスライスがappendされてから:%v",arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスがappendされてからのメモリアドレス:%p", &arr))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスがappendされてから参照するArrayのメモリアドレス:%p", arr))
}
スライス参照のArrayのメモリアドレス:0xc000016100
変更前:[1 2 3]
関数に渡す前スライスのメモリアドレス:0xc00000c060
関数に渡されたスライスのメモリアドレス:0xc00000c0c0
関数に渡されたスライスが参照するArrayのメモリアドレス:0xc000016100
関数に渡されたスライスがappendされてから:[1 2 3 4]
関数に渡されたスライスがappendされてからのメモリアドレス:0xc00000c0c0
関数に渡されたスライスがappendされてから参照するArrayのメモリアドレス:0xc000018180
変更後:[1 2 3]

go slice parameter.005.jpeg

図のように、関数内に渡されたスライスがappend操作後、新しいunderlying Arrayが作られ、関数内のスライスはその新しいArrayを参照になりました。関数外のスライスは古いArrayに参照にしています。

この場合、スライスのポインタを関数に渡したら、関数内のappend操作による変化は関数の外側のスライスに及ぼします

package main

import "fmt"

func main()  {
    arr := []int{1,2,3}
    fmt.Println(fmt.Sprintf("スライス参照のArrayのメモリアドレス:%p", arr))
    fmt.Println(fmt.Sprintf("変更前:%v",arr))
    fmt.Println(fmt.Sprintf("関数に渡す前スライスのメモリアドレス:%p", &arr))
    arrPointer := &arr
    fmt.Println(fmt.Sprintf("関数に渡す前スライスポインタのメモリアドレス:%p",&arrPointer))
    foo(arrPointer)
    fmt.Println(fmt.Sprintf("変更後:%v",arr))
}


func foo(arrPointer *[]int) {
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタのメモリアドレス:%p", &arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスのメモリアドレス:%p", arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するArrayのメモリアドレス:%p", *arrPointer))
    *arrPointer = append(*arrPointer,4)
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスがappendされてから:%v",arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスがappendされてからのメモリアドレス:%p", arrPointer))
    fmt.Println(fmt.Sprintf("関数に渡されたスライスポインタが参照するスライスがappendされてから参照するArrayのメモリアドレス:%p", *arrPointer))
}
スライス参照のArrayのメモリアドレス:0xc000016100
変更前:[1 2 3]
関数に渡す前スライスのメモリアドレス:0xc00000c060
関数に渡す前スライスポインタのメモリアドレス:0xc00000e030
関数に渡されたスライスポインタのメモリアドレス:0xc00000e038
関数に渡されたスライスポインタが参照するスライスのメモリアドレス:0xc00000c060
関数に渡されたスライスポインタが参照するArrayのメモリアドレス:0xc000016100
関数に渡されたスライスポインタが参照するスライスがappendされてから:&[1 2 3 4]
関数に渡されたスライスポインタが参照するスライスがappendされてからのメモリアドレス:0xc00000c060
関数に渡されたスライスポインタが参照するスライスがappendされてから参照するArrayのメモリアドレス:0xc000018180
変更後:[1 2 3 4]

go slice parameter.006.jpeg
図のように、関数内はスライスのポインタから関数の外側のスライスを操作します。外側のスライスだけ存在しています。
実は外側のスライスは関数内の操作によって、参照するunderlying Arrayを作り直しています。

まとめ

go言語は関数にポインタ渡しはありません。
スライスを渡しているときに、実はスライスをコピーして渡しています。
関数に渡す時に、関数内と関数外のスライスは同じArrayを参照しています。
スライスはunderlying Arrayを参照してます
スライスのポインタはスライスを参照しています

参考資料

16
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
8