在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!]

结论

相关推荐
东风破_4 小时前
danci 2:创建的单词书到底存在哪里?从 Supabase 一路理解 ORM、Drizzle 和 RLS
数据库·后端·node.js
东风破_4 小时前
danci 项目(三):从 JSON 数据到 AI Coding,真实项目里的数据清洗、Prompt 和工程规范
前端·后端·node.js
东风破_5 小时前
danci 项目(一):从需求到架构,一个单词学习系统为什么会这样设计
前端·后端·node.js
jay神6 小时前
【计算机毕业设计】基于SpringBoot的程序教学辅助系统
java·前端·vue.js·spring boot·后端·毕业设计·课程设计
2601_962073819 小时前
Spring全面详解(基础版)
java·后端·spring
IT_陈寒9 小时前
Vite打包时的静态资源坑,我帮你踩过了
前端·人工智能·后端
考虑考虑9 小时前
kubectl命令
运维·后端·自动化运维
运维开发笔记10 小时前
8.2 Go Struct 方法学习笔记
go
第五页的你10 小时前
ElasticSearch结合本地消息表实现搜索功能
后端
谢亮_vipxieliang10 小时前
ValidX分组验证详解:不同场景使用不同验证规则
java·spring boot·后端·spring cloud·eclipse·hibernate