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

原文地址: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!]

结论

相关推荐
程序员小假9 分钟前
你会不会使用 SpringBoot 整合 Flowable 快速实现工作流呢?
java·后端
明月与玄武38 分钟前
快速掌握Django框架设计思想(图解版)
后端·python·django
陪我一起学编程39 分钟前
关于ORM增删改查的总结——跨表
数据库·后端·python·django·restful
南囝coding1 小时前
这个 361K Star 的项目,一定要收藏!
前端·后端·github
虎鲸不是鱼1 小时前
Spring Boot3流式访问Dify聊天助手接口
java·spring boot·后端·大模型·llm
onlooker66661 小时前
Go语言底层(五): 深入浅出Go语言的ants协程池
开发语言·后端·golang
武子康1 小时前
Java-46 深入浅出 Tomcat 核心架构 Catalina 容器全解析 启动流程 线程机制
java·开发语言·spring boot·后端·spring·架构·tomcat
寻月隐君2 小时前
Solana 开发实战:Rust 客户端调用链上程序全流程
后端·rust·web3
丘山子3 小时前
别再滥用 None 了!这才是 Python 处理缺失值的好方法
后端·python·面试
error_cn3 小时前
postgresql视图与触发器
后端