原文地址:www.willem.dev/articles/sh...
如果你对Go语言的切片还不是很熟悉,那么,在将切片作为参数传递的时候,最好是用指针,而非传值。例如:
go
s := []string{"👁", "👃", "👁"}
doSomething(&s)
andAgain(&s)
但是,这也并没有什么优点。
实际上,通过传递切片,你已经传递了一个指针:切片包含指向底层数组的指针。如下:
通过使用指向切片(slice)的指针,你本质上是在使用双指针(一个指向切片的指针,它反过来指向一个数组)。
这不仅仅是一个概念问题。这种额外的间接调用将使你的代码更加复杂。
复杂性1:指针可能会是nil 所有与切片一起使用的内置 Go 函数(如 append()、copy()、len() 和 cap())都要求切片是传值输入。而不是指向切片的指针。 在调用这些函数之前,您需要取消引用指针。
你只能对非nil的指针进行取消引用指针,因为一个nil指针不会指向任何值。
如果,你在代码中引用一个nil指针,那么会引起panic。如下:
go
panic: runtime error: invalid memory address or nil pointer dereference
因此,在引用指针时,你必须要检查指针是否是nil。如下:
go
package main
import "fmt"
func main() {
s := []string{"👁", "👃", "👁"}
printLen(&s)
printLen(nil)
}
func printLen(sPtr *[]string) {
// before we dereference the pointer we check if it is not nil.
if sPtr != nil {
// dereference the pointer when getting the length.
fmt.Printf("the length of the slice is: %d\n", len(*sPtr))
} else {
fmt.Println("pointer is nil")
}
}
这种做法会导致大量的额外代码,同时,如果你忘记了检查指针是否为nil,就会有panic的风险。
复杂性2:slice可能为nil 和指针类似,slice本身可能是nil。
通常不需要检查slice是否为nil(所有内置的切片函数都能处理它),但有时这是必要的。 如果你要检查slice是否为nil,首先需要检查指针是否为 nil,然后检查 slice 是否为 nil。 如下:
go
if sPtr != nil && *sPtr == nil {
// do something when slice is nil.
}
当阅读这样的代码时,会付出大量额外的精力。因此,不使用指针的方案会更容易接受。如下:
go
if s == nil {
// do something when slice is nil.
}
复杂度3:其他Go研发者 除非有特殊原因,否则其他 Go 程序员不会在他们的代码中使用切片指针。为了与其他程序员顺利合作,尽可能地遵守通用惯例是一个好主意。
合适使用切片指针
正如你所看到的,一般来说,你应该使用切片而不是指向切片的指针。 但是,在某些情况下,指向切片的指针是有用的吗?
解码和反序列化数据
使用切片指针的最常见情况应该是解码或反序列化数据时。 标准库中的 gob 、 xml 和 json 包都是以类似方式工作的函数和方法:
go
func (d *Decoder) Decode(v any) error
func Unmarshal(data []byte, v any) error
对于这两个函数,v是需要指向你想解码的变量或将数据反序列化到其中的指针。这意味着如果你想解码或将数据反序列化到切片中,你需要传递一个指向切片的指针。
例如,如果你想把一个json数组反序列化到一个切片中:
go
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
data := []byte("[\"👁\", \"👃\", \"👁\"]")
var target []string
// unmarshal needs a pointer to the target slice.
err := json.Unmarshal(data, &target)
if err != nil {
log.Fatal(err)
}
fmt.Println(target)
}
自定义类型的指针接收者
可以定义一个自定义类型,该类型以切片作为其基础类型。例如:
go
type greetings []string
定义了一个叫做 greetings的类型,该类型以字符串切片为基础类型。 如果我们想实现一个操作切片的方法,一般有两个选择:
- 使用值接收器。
- 使用指针接收器。
假设我们想在每次调用 AddOne 方法时都需要append一个问候语。
如果我们使用值接收器,那么调用该方法后会返回一个新的 greetings的值,如下:
go
func (g greetings) AddOne() greetings {
return append(g, "hello!")
}
并且将由调用者来处理调用 AddOne 的结果。
go
var g greetings
g = g.AddOne()
fmt.Println(g) // prints: [hello!]
如果我们使用指针接收器,就可以在方法内部直接使用接收器,并且也不需要再返回一个新的 greetings的值。如下:
go
func (g *greetings) AddOne() {
*g = append(*g, "hello!")
}
现在,调用者看起来像这样:
go
g := &greetings{}
g.AddOne()
fmt.Println(g) // prints: &[hello!]