Golang中返回指针切片与切片指针的区别

Golang中返回指针切片与切片指针的区别 你好,

我想返回 []Category。 但我很困惑哪种是更好的选择?

func GetCategories() (*[]Category, error) {}
func GetCategories() ([]*Category, error) {}

在这两者之间,Go 通常遵循哪种做法?

这样做可以吗?

func GetCategories() (*[]*Category, error) {} // 尚未尝试过。
10 回复

返回指向切片的指针意义不大。

更多关于Golang中返回指针切片与切片指针的区别的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


啊,我之前没想到可以使用具名的切片类型。说得好!

我想返回 Category。

那就返回它。 传递指针总是需要有理由的。默认情况下不应使用指针。

返回一个切片的指针没有太大意义。

有意义的。

Categories 是一个包含许多字段的结构体。因此,我认为通过切片传递地址会比直接传递数据更快。

所以,我们是否应该只在需要更改原始数据时才使用指针?否则,我们只需传递数据的副本即可。我的理解对吗?

我想补充一点,在大多数情况下,返回数据的副本是相当高效的。除非 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,原因如下:

  1. 避免不必要的指针间接访问:切片本身已经是引用类型
  2. 内存效率:允许修改切片中的单个元素而不影响其他调用者
  3. 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语言习惯的做法。

回到顶部