Golang中返回指针切片与切片指针的区别
Golang中返回指针切片与切片指针的区别 你好,
我想返回 []Category。 但我很困惑哪种是更好的选择?
func GetCategories() (*[]Category, error) {}
func GetCategories() ([]*Category, error) {}
在这两者之间,Go 通常遵循哪种做法?
这样做可以吗?
func GetCategories() (*[]*Category, error) {} // 尚未尝试过。
啊,我之前没想到可以使用具名的切片类型。说得好!
我想返回 Category。
那就返回它。 传递指针总是需要有理由的。默认情况下不应使用指针。
返回一个切片的指针没有太大意义。
有意义的。
我想补充一点,在大多数情况下,返回数据的副本是相当高效的。除非 Category 包含一大块二进制数据(比如视频之类的),否则在实际应用中你不太可能遇到问题。所以,这很可能是一种过早的优化。你的瓶颈几乎肯定出现在另一个层面(例如数据访问,或者为了返回那一大块分类数据而进行的大量连接操作)。
同意。我可以理解传递数组指针的做法,但切片则不然,因为切片本身持有对底层数组的引用:
切片持有对底层数组的引用。如果你将一个切片赋值给另一个切片,两者将引用同一个数组。如果一个函数接收切片作为参数,它对切片元素所做的修改对调用者是可见的,这类似于传递了指向底层数组的指针。
drornir.dev:
mje: 返回指向切片的指针没有太大意义。
有意义的。
你愿意详细说明一下吗?我想不出任何情况下返回一个指向切片的指针是个好主意。它确实能让你区分 p == nil 和 *p == nil,但我很有兴趣看看使用这个结果的代码。我想可能会有更好的方法来实现(例如,返回一个指向包含单个切片字段的结构体的指针,也许?)。
不过,我确实同意你的观点,你应该有使用指针的理由,如果你不确定是否需要,那么你就不需要指针。
如果你传递或返回一个单一的 Category,那么所有字段都会被复制一份。如果你将一个 Category 放入切片中,那么在那个时刻所有字段都会被复制一份。如果你传递或返回一个 Category 切片,那么 Category 数据不会有额外的副本。如果你传递或返回一个指向 Category 的指针,那么不会有副本(除了指针的 8 字节),但指针所指向的 Category 可能会被两段代码修改,这可能导致错误。将指针放入切片中也不会复制 Category,并且你可能会遇到类似的共享情况。返回一个 Category 切片与返回一个 Category 指针切片在复制的数据量上没有区别。返回一个切片允许切片中的 Category 或指针可能被共享,因为支持切片的数据对该切片的所有副本都是可见的。返回一个指向切片的指针意义不大,除了复制指针意味着复制 8 字节,而复制切片意味着复制 24 字节。也许有人可以使用不安全访问来修改切片的内部数据,但这是极不规范的。(字节计数在不同环境中可能不同,但重点是复制切片仅比复制指针稍微昂贵一点。)
乐意之至!
package main
import "fmt"
type ErrorsAccumulator []error
func (ea *ErrorsAccumulator) Add(err error) {
*ea = append(*ea, err)
}
func NewEA() *ErrorsAccumulator {
ea := make(ErrorsAccumulator, 0)
return &ea
}
func main() {
ea := NewEA()
ea.Add(fmt.Errorf("ee"))
fmt.Println(ea, *ea)
}
虽然这个例子有点刻意,但它试图说明共享状态可以仅用一个切片来表示,并且像对任何结构体取指针一样,对这个切片取指针也是有意义的。
我会推荐这种做法吗?不会。但我倾向于避免使用指针,无论它们指向什么。我更愿意将切片和映射包装在一个结构体中。
所以,我实际上会这样实现上面的例子:
package main
import "fmt"
type ErrorsAccumulator struct {
errs []error
}
func (ea *ErrorsAccumulator) Add(err error) {
(*ea).errs = append((*ea).errs, err)
}
func NewEA() *ErrorsAccumulator {
return &ErrorsAccumulator{errs: make(ErrorsAccumulator, 0)}
}
...
在Go语言中,这三种返回方式有本质区别,分别适用于不同场景:
1. *[]Category - 指向切片的指针
func GetCategories() (*[]Category, error) {
categories := []Category{
{ID: 1, Name: "Electronics"},
{ID: 2, Name: "Books"},
}
return &categories, nil
}
// 调用示例
cats, err := GetCategories()
if err != nil {
log.Fatal(err)
}
// 需要通过 * 解引用
for i := range *cats {
(*cats)[i].Name = "Updated: " + (*cats)[i].Name
}
2. []*Category - 包含指针的切片
func GetCategories() ([]*Category, error) {
categories := []*Category{
{ID: 1, Name: "Electronics"},
{ID: 2, Name: "Books"},
}
return categories, nil
}
// 调用示例
cats, err := GetCategories()
if err != nil {
log.Fatal(err)
}
// 直接操作切片中的指针
for _, cat := range cats {
cat.Name = "Updated: " + cat.Name
}
3. *[]*Category - 指向指针切片的指针(不推荐)
func GetCategories() (*[]*Category, error) {
categories := &[]*Category{
{ID: 1, Name: "Electronics"},
{ID: 2, Name: "Books"},
}
return categories, nil
}
Go语言的惯用做法
最常用的是 []*Category,原因如下:
- 避免不必要的指针间接访问:切片本身已经是引用类型
- 内存效率:允许修改切片中的单个元素而不影响其他调用者
- API简洁性:调用者无需解引用即可使用
// 推荐做法
func GetCategories() ([]*Category, error) {
var categories []*Category
// 从数据库或其他源获取数据
rows, err := db.Query("SELECT id, name FROM categories")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var cat Category
if err := rows.Scan(&cat.ID, &cat.Name); err != nil {
return nil, err
}
categories = append(categories, &cat)
}
return categories, nil
}
性能考虑
[]Category:值切片,适合小结构体,复制成本低[]*Category:指针切片,适合大结构体或需要修改的场景*[]Category:很少使用,除非需要原地修改切片本身(如重新分配)
实际示例
type Category struct {
ID int
Name string
}
// 方案1:值切片(适合小对象)
func GetCategoriesAsValues() ([]Category, error) {
return []Category{
{1, "Electronics"},
{2, "Books"},
}, nil
}
// 方案2:指针切片(推荐,最常用)
func GetCategoriesAsPointers() ([]*Category, error) {
return []*Category{
{1, "Electronics"},
{2, "Books"},
}, nil
}
// 方案3:切片指针(特定场景)
func GetCategoriesSlicePtr() (*[]Category, error) {
cats := []Category{
{1, "Electronics"},
{2, "Books"},
}
return &cats, nil
}
在大多数情况下,选择 []*Category 是最符合Go语言习惯的做法。


