在结构字段中使用指针的区别

omu*_*bek 4 struct pointers go

我们可以通过这种方式在 golang 中创建结构体。下面的例子:这两者之间有什么区别?

// Usual way
type Employee struct {
    firstName string    `json:"name"`
    salary    int       `json:"salary"`
    fullTime  bool      `json:"fullTime"`
    projects  []Project `json:"projects"`
}

// Un-usal way with pointers
type Employee struct {
    firstName *string    `json:"name"`
    salary    *int       `json:"salary"`
    fullTime  *bool      `json:"fullTime"`
    projects  *[]Project `json:"projects"`
}
Run Code Online (Sandbox Code Playgroud)

有没有像内存这样的权衡?

更新:

假设以下函数:

// this function consumes MORE memory
func printEmployeeWithoutPointer(employee Employee) {
    // print here
}

// this function consumes LESS memory
func printEmployeeWithPointer(employee *Employee) {
    // print here
}
Run Code Online (Sandbox Code Playgroud)

Eli*_*gem 11

是的,有很多事情需要考虑。首先:让我们从指针示例中明显的语法错误开始:

type Employee struct {
    FirstName *string `json:"name"`
    Salary    *int    `json:"salary"`
    FullTime  *bool   `json:"fullTime"`
}
Run Code Online (Sandbox Code Playgroud)

所以我把星号移到了类型上,并且我把字段大写了。该encoding/json包使用反射来设置字段的值,因此需要将它们导出。

看到您正在使用 json 标签,让我们从简单的事情开始:

type Foo struct {
    Bar string  `json:"bar"`
    Foo *string `json:"foo,omitempty"`
}
Run Code Online (Sandbox Code Playgroud)

当我解组没有bar值的消息时,该Bar字段将只是一个空字符串。这使得很难确定该字段是否已发送。特别是在处理整数时:如何区分未发送的字段与发送值为 0 的字段之间的区别?
使用指针字段并指定omitempty允许您这样做。如果该字段未在 JSON 数据中指定,则结构中的字段将为nil,否则:它将指向值为 0 的整数。

当然,必须检查指针是否为零可能很乏味,这会使代码更容易出错,因此只有在有实际原因需要区分未设置的字段时才需要这样做,并且一个零值。


陷阱

指针允许您更改它们指向的值

让我们继续讨论指针本身带来的风险。假设您的Employee结构具有指针字段,并且调用的类型EmployeeV相同但具有值字段,请考虑以下函数:

func (e Employee) SetName(name string) {
    if e.Firstname == nil {
        e.Firstname = &name
        return
    }
    *e.Firstname = name
}
Run Code Online (Sandbox Code Playgroud)

现在这个功能只会在一半的时间里工作。您正在呼叫SetName一个价值接收者。如果Firstname是 nil,那么您将在原始变量的副本上设置指针,并且您的变量不会反映您在函数中所做的更改。如果Firstname 设置,但是,副本将指向相同的字符串作为你的原始变量,而指针指向的值得到更新。那很糟。

在 上实现相同的功能EmployeeV,但是:

func (e EmployeeV) SetName(name string) {
    e.Firstname = name
}
Run Code Online (Sandbox Code Playgroud)

它根本不会永远工作。您将始终更新副本,并且更改不会影响您调用该SetName函数的变量。出于这个原因,在 go 中,做这样的事情的惯用方式是:

type Employee struct {
    Firstname string
    // other fields
}

func (e *Employee) SetName(name string) {
    e.Firstname = name
}
Run Code Online (Sandbox Code Playgroud)

因此,我们正在更改方法以使用指针接收器。

数据竞争

与往常一样:如果您使用指针,您实际上是在允许代码操作直接指向的内存。鉴于 golang 是一种以促进并发而闻名的语言,并且访问相同的内存意味着您有创建数据竞争的风险:

func main() {
    n := "name"
    e := Employee{
        Firstname: &n,
    }
    go func() {
         *e.Firstname = "foo"
    }()
    race(e)
}

func race(e Employee) {
    go race(e)
    go func() {
        *e.Firstname = "in routine"
    }()
    *e.Firstname = fmt.Sprintf("%d", time.Now().UnixNano())
}
Run Code Online (Sandbox Code Playgroud)

Firstname在许多不同的例程中访问该字段。它的最终价值是多少?你还知道吗?golang 竞争检测器很可能会将此代码标记为潜在的数据竞争。


在内存使用方面:像 ints 或 bools 这样的单个字段真的不是你应该担心的事情。如果您正在传递一个相当大的结构,并且您知道它是安全的,那么传递一个指向该结构的指针可能是个好主意。再说一次,通过指针访问值而不是直接访问它们不是免费的:间接增加了一个小的开销。


Leo*_*eon 6

我们使用指针来共享数据,但这并不总是意味着它具有更高的内存效率或更高的性能。Go 在复制数据方面非常出色且快速。

当谈到结构时,使用指针的一个常见原因是指针可以有nil值,而原语不能。如果你需要一个带有 optionals 字段的结构,你会使用指针

如果您正在反序列化 JSON,那么您可以使用omitempty. 这里 fullTime 是可选的

type Employee struct {
    firstName string `json:"name"`
    salary int `json:"salary"`
    fullTime *bool `json:"fullTime,omitempty"`
}
Run Code Online (Sandbox Code Playgroud)

使用 JSON 时的性能

如果您将 JSON 反序列化为指针以希望节省内存,则不会。从 JSON 的角度来看,每个项目都是独一无二的,因此没有数据共享。您将使用更多内存,因为现在每个值都必须存储一个值和一个指向该值的指针。它会更慢,因为您需要一直取消引用指针

  • @OmurbekKadyrbekov 切片是带有指向底层数组的指针以及 len 和 cap 信息的标头(https://blog.golang.org/go-slices-usage-and-internals)。出于内存效率的原因,请勿使用指针进行切片。 (3认同)