1. 指针的含义及其操作
指针也是变量,值为变量的地址,即指针指向变量的内存地址。指针与变量的地址相关联,所以可通过指针修改变量的值,
go 中指针的操作比较简单,只需记住两个操作:& 和 *。
取地址
使用 & 放在变量前面可对变量进行“取地址”操作。
a := 10
b := &a
取值
使用 * 可对指针所指向的地址进行取值。
a := 10
b := &a
c := *b
当函数的参数为指针时,直接操作指针能获取指针指向的类型的值:
func testfFunc(param *test) {
if param != nil {
fmt.Println(param.value)
}
}
2. 指针 vs 值
首先,我们写一个方法来模仿append方法:
值
我们首先要声明一个已命名的类型来绑定该方法, 然后使该方法的接收者成为该类型的值。
这里的接收者接收的是值的副本,修改之后调用者的值不会发生改变。
type ByteSlice []int
func (slice ByteSlice) Append(data []byte) []byte {
// 方法主体
}
在这种方式下,我们仍然需要该方法返回更新后的切片。
指针
我们可以重新定义该方法,将一个指向 ByteSlice 的指针作为该方法的接收者, 这样该方法就能重写调用者提供的切片,并且没有返回值。
这里的接收者接收的是引用,修改之后,调用者的值会发生改变。
func (p *ByteSlice) Append(data []byte) {
slice := *p
// 主体同上,只是没有返回值
*p = slice
}
我们将函数修改为与标准 Write 类似的方法,如下:
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// 同上。
*p = slice
return len(data), nil
}
那么类型 *ByteSlice 就满足了标准的 io.Writer 接口。
我们将 ByteSlice 的地址传入,因为只有 *ByteSlice 才满足 io.Writer。以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。
“之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。在我们的例子中,变量 b 是可寻址的,因此我们只需通过 b.Write 来调用它的 Write 方法,编译器会将它重写为 (&b).Write。” 引自《Effective Go》
总结
正如上面所说,值方法可通过指针和值调用,而指针方法只能通过指针来调用。但后半句有个例外,即如果值是可寻址的,则该值便可调用指针方法。
这也解开了我刚学习 Go 时的一个心结:
var command1 command // 创建对象,类型为对象类型
command2 := command{CommandNo: 1, CommandName: a} // 对象类型
command3 := new(command) // 指针类型
不管是对象类型还是指针类型,都能够调用如下方法:(该方法的接受者为指针类型)
func (command *command) excute() {
}
command1.excute()
command2.excute()
command3.excute()
command1 和 command2都为对象类型,按理说无法调用 excute() 方法。能调用说明这个对象值是可寻址的,编译器会将它重写为 (&command).excute()。
3. new 和 make
new 和 make 都用于内存分配。
不同的是,make只用于slice、map 和 channel的初始化,返回值是一个类型。
new 只用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向内存的指针。
new
new分配内存时只是将内存置零,不能进行初始化,都为零值。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
new创建的变量为指针类型(即地址),并且 new(c) 和 &c{} 等价。
make
make 为切片、映射和信道分配内存时,返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。
make([]int, 10) // 创建一个长度和容量都为10的切片