プログラムでは、変数は変数名と変数の内容に分けられます。変数の内容のストレージは通常、ヒープとスタックに割り当てられます。Goでは、変数を渡す方法として、値を渡す方法と参照を渡す方法の2つがあります。その中で、値渡しは変数コンテンツを変数名に直接添付し、参照渡しは変数名に添付された変数コンテンツのアドレスを渡します。
Golangでそれを行う方法
面接官が面接中に「Goパラメーターはどのように渡されますか?」と尋ねた場合、どのように答えますか?
この質問に対する答えは本当に1つだけです。Golangでのすべての型の転送は、参照ではなく値の転送によって実現されるため、ポインターの転送でさえ、ポインターをコピーすることによって行われます。さらに、基になるデータをラップする一部のデータ構造では、値を渡すプロセスで、インスタンスのポインターのみがコピーされ、基になるデータによって公開されるポインターはコピーされません。
簡単に理解するために、例としてGoバージョン1.8のスライスを取り上げましょう。
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true) // 申请内存
}
スライスが初期化プロセス中にランタイムでmakeslice関数を呼び出し、この関数がスライスのアドレスを受け入れられた変数に返すことがわかります。
type slice struct {
array unsafe.Pointer // 底层数组的地址
len int
cap int
}
// 初始化过程
p := make([]int,0)
fmt.Printf("变量p的地址%p", &p)
fmt.Printf("slice的地址%p\n", p)
上に印刷したときに表示されるのは、Goが内部で自動間接参照を実装しているためです(つまり、Goが内部で実装している間接参照操作)。受信は、自動逆参照中にポインター型から値型に変換されます。ちなみに、参照が自動的に取得されると、レシーバーは値型からポインター型に変換されます。
自動間接参照が実装されていない場合はどうなりますか?自動間接参照が実装されていない場合は、次のようになります。
// 当我们打印变量p的时候,实际过程是发生了这样的变化
// 只是猜测,当然发生解引用是一定的
// & 取地址操作符
// * 根据地址取值操作 也称之为解引用运算法,间址运算符
// 1. 获取指针地址 &p
// 2. 获取array的地址 &((&p).array)
// 3. 获取底层数组实际内容 *&((&p).array)
自動借用を実装していない関数転送プロセスもポインタをコピーして渡され、内容は次のとおりです。
package main
import (
"fmt"
)
func change(p1 []int) {
fmt.Printf("p1的内存地址是: %p\n", &p1) // p1的内存地址是: 0xc0000a6048
fmt.Printf("函数里接收到slice的内存地址是:%p\n", p1) // 函数里接收到slice的内存地址是:0xc00008c030
p1 = append(p1, 30)
}
func main() {
p := make([]int, 3) // 抛出一个指针
p = append(p, 20)
fmt.Printf("p的内存地址是: %p\n", &p) // p的内存地址是: 0xc00009a018
fmt.Printf("slice的内存地址是:%p\n", p) // slice的内存地址是:0xc00008c030
change(p) // 重新生成一份地址p1 指向slice地址
fmt.Printf("修改之后p的内存地址 %p\n", &p) // 修改之后p的内存地址 0xc00009a018
fmt.Printf("修改之后slice的内存地址 %p\n", p) // 修改之后slice的内存地址 0xc00008c030
fmt.Println("修改之后的slice:", p) // 修改之后的slice [0 0 0 20]
fmt.Println(*&p) // [0 0 0 20]
}
関数転送のプロセスでコピーされるのは、スライス内の基になる配列へのポインターではなく、makeslice関数によって返されるポインターであることに注意してください。
ソースコードの実装
古い記事を読んだときに、次のステートメントを見たことがあるかもしれません。makeはsliceのインスタンスを返します。しかし実際には、Golang 1.2バージョンmakeがインスタンスへのポインターを返した後、このステートメントは古くなっています。
github prアドレス:https ://github.com/golang/go/commits/dev.boringcrypto.go1.12/src/runtime/slice.go
拡大
実はスライスに似た地図ちゃんがあります。
最初にマップについて説明しましょう。マップの公式Webサイト定義:「Goはハッシュテーブルを実装する組み込みのマップタイプを提供します。マップタイプはポインタやスライスなどの参照型です。」また、chanとmapもポインタです。スライスの原理に似た2つを意味します。
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
...
}
func makechan(t *chantype, size int) *hchan {
...
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
...
}
通常の使用に注意を払わないと、次のような不要なトラブルが発生します。
package main
import "fmt"
type InfoIns struct {
Name string
info []string
}
func NewInfoIns() InfoIns{
return InfoIns{
Name: "",
info: nil,
}
}
func (n *InfoIns) SetInfo(info []string){
n.info = info
}
func main(){
infoIns := NewInfoIns()
info := []string{"p1", "p2", "p3"}
infoIns.SetInfo(info)
info[1] = "p4"
fmt.Println(infoIns.info) // [p1 p4 p3]
}
ここのInfoInsは、SetInfoの後に情報のアドレスを格納します。将来的に情報が変更されると、それに応じてInfoInsのコンテンツも変更されます。解決策は、SetInfo中にアドレスを再申請することです。
func (n *InfoIns) SetInfo(info []string){
n.info = make([]string, len(info))
copy(n.info, info)
}
脚注
GolandでGoソースコードを表示する方法:Ctrl + Shift + fグローバル検索で、[スコープ内のすべての場所]を選択します。
おすすめの読み物
ある記事では、IPアドレスについてそれらについて説明しています