likes
comments
collection
share

在Go中,是否该使用切片的指针?

作者站长头像
站长
· 阅读数 32

原文地址:www.willem.dev/articles/sh…

如果你对Go语言的切片还不是很熟悉,那么,在将切片作为参数传递的时候,最好是用指针,而非传值。例如:

s := []string{"👁", "👃", "👁"}
doSomething(&s)
andAgain(&s)

在Go中,是否该使用切片的指针?

但是,这也并没有什么优点。

实际上,通过传递切片,你已经传递了一个指针:切片包含指向底层数组的指针。如下:

通过使用指向切片(slice)的指针,你本质上是在使用双指针(一个指向切片的指针,它反过来指向一个数组)。 在Go中,是否该使用切片的指针?

这不仅仅是一个概念问题。这种额外的间接调用将使你的代码更加复杂。

复杂性1:指针可能会是nil 所有与切片一起使用的内置 Go 函数(如 append()、copy()、len() 和 cap())都要求切片是传值输入。而不是指向切片的指针。 在调用这些函数之前,您需要取消引用指针。

你只能对非nil的指针进行取消引用指针,因为一个nil指针不会指向任何值。 在Go中,是否该使用切片的指针?

如果,你在代码中引用一个nil指针,那么会引起panic。如下:

panic: runtime error: invalid memory address or nil pointer dereference

因此,在引用指针时,你必须要检查指针是否是nil。如下:

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。 在Go中,是否该使用切片的指针?

通常不需要检查slice是否为nil(所有内置的切片函数都能处理它),但有时这是必要的。 如果你要检查slice是否为nil,首先需要检查指针是否为 nil,然后检查 slice 是否为 nil。 如下:

if sPtr != nil && *sPtr == nil {
	// do something when slice is nil.
}

当阅读这样的代码时,会付出大量额外的精力。因此,不使用指针的方案会更容易接受。如下:

if s == nil {
	// do something when slice is nil.
}

复杂度3:其他Go研发者 除非有特殊原因,否则其他 Go 程序员不会在他们的代码中使用切片指针。为了与其他程序员顺利合作,尽可能地遵守通用惯例是一个好主意。

合适使用切片指针

正如你所看到的,一般来说,你应该使用切片而不是指向切片的指针。 但是,在某些情况下,指向切片的指针是有用的吗?

解码和反序列化数据

使用切片指针的最常见情况应该是解码或反序列化数据时。 标准库中的 gob 、 xml 和 json 包都是以类似方式工作的函数和方法:

func (d *Decoder) Decode(v any) error

func Unmarshal(data []byte, v any) error

对于这两个函数,v是需要指向你想解码的变量或将数据反序列化到其中的指针。这意味着如果你想解码或将数据反序列化到切片中,你需要传递一个指向切片的指针。

例如,如果你想把一个json数组反序列化到一个切片中:

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)
}

自定义类型的指针接收者

可以定义一个自定义类型,该类型以切片作为其基础类型。例如:

type greetings []string

定义了一个叫做 greetings的类型,该类型以字符串切片为基础类型。 如果我们想实现一个操作切片的方法,一般有两个选择:

  • 使用值接收器。
  • 使用指针接收器。

假设我们想在每次调用 AddOne 方法时都需要append一个问候语。

如果我们使用值接收器,那么调用该方法后会返回一个新的 greetings的值,如下:

func (g greetings) AddOne() greetings {
	return append(g, "hello!")
}

并且将由调用者来处理调用 AddOne 的结果。

var g greetings
g = g.AddOne()
fmt.Println(g) // prints: [hello!]

如果我们使用指针接收器,就可以在方法内部直接使用接收器,并且也不需要再返回一个新的 greetings的值。如下:

func (g *greetings) AddOne() {
	*g = append(*g, "hello!")
}

现在,调用者看起来像这样:

g := &greetings{}
g.AddOne()
fmt.Println(g) // prints: &[hello!]

结论

转载自:https://juejin.cn/post/7307146429003759654
评论
请登录