Golang中如何处理包含嵌入引用类型结构体的拷贝与引用问题

Golang中如何处理包含嵌入引用类型结构体的拷贝与引用问题 你好,

我想知道下面哪种使用结构体的方式是正确的,以及为什么?

谢谢

type UserStorage struct {
    database *sql.DB
    timeout  time.Duration
}

db1 := UserStorage{ /* 如上所示传递参数 */ }   // 这个?
db2 := &UserStorage{ /* 如上所示传递参数 */ }  // 还是这个?

// 然后将 `db1` 或 `db2` 注入到你需要的地方。
4 回复

是的,你是对的。

更多关于Golang中如何处理包含嵌入引用类型结构体的拷贝与引用问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果我反复将 db1 传递给任何函数,每次都会在内存中创建一个新的 UserStorage 副本,并且一旦函数退出,旧的副本就会被移除,这一点我是知道的。我假设 db1 的每个副本将始终使用现有的相同 database 属性引用,因为它已经是内存中 *sql.DB 的引用。我的假设正确吗?因此,内存中始终只有一份 *sql.DB 的副本。如果我使用 db2,情况也是如此。

这两种方式没有绝对的正确与否,只是使用方式不同。

如果一个函数 updateStorage 期望接收一个 *UserStorage 指针,那么你必须这样传递 db1updateStorage(&db1),而传递 db2 时只需 updateStorage(db2)。如果有一个 readStorage 函数期望接收一个 UserStorage 值,那么你必须分别这样传递 db1db2readStorage(db1)readStorage(*db2)

在 Go 语言中,函数参数总是按值传递,所以这个假设的 readStorage 函数会得到一个 UserStorage 的副本,并且无法修改传入的原始值。接收指针的函数则可以修改这些指针所指向的值。你可以根据这些细节来决定使用 UserStorage 还是 *UserStorage

在Go中,选择使用值类型还是指针类型主要取决于结构体的使用场景和语义。对于你提供的UserStorage结构体,由于它包含一个*sql.DB引用类型字段,通常建议使用指针。

原因分析

  1. database字段已经是引用类型*sql.DB本身就是一个指针,无论结构体是值类型还是指针类型,这个字段都是共享的
  2. 避免不必要的拷贝:即使结构体不大,使用指针可以确保传递的是引用而不是副本
  3. 语义一致性UserStorage通常表示一个资源持有者,使用指针更符合其角色

推荐使用指针方式

// 推荐:使用指针
storage := &UserStorage{
    database: db,  // *sql.DB
    timeout:  30 * time.Second,
}

// 在函数间传递时也是指针
func NewService(storage *UserStorage) *Service {
    return &Service{storage: storage}
}

两种方式的区别示例

type UserStorage struct {
    database *sql.DB
    timeout  time.Duration
}

func main() {
    // 方式1:值类型
    db1 := UserStorage{
        database: initDB(),
        timeout:  30 * time.Second,
    }
    
    // 方式2:指针类型  
    db2 := &UserStorage{
        database: initDB(),
        timeout:  30 * time.Second,
    }
    
    // 修改测试
    modifyStorage(db1)  // 传递副本,原db1不变
    modifyStoragePtr(db2) // 传递引用,原db2会被修改
}

func modifyStorage(s UserStorage) {
    s.timeout = 60 * time.Second // 只修改副本
}

func modifyStoragePtr(s *UserStorage) {
    s.timeout = 60 * time.Second // 修改原对象
}

实际应用中的建议

// 工厂函数通常返回指针
func NewUserStorage(db *sql.DB, timeout time.Duration) *UserStorage {
    return &UserStorage{
        database: db,
        timeout:  timeout,
    }
}

// 方法接收器也使用指针
func (s *UserStorage) QueryUser(id int) (*User, error) {
    // 需要访问database字段
    // 如果使用值接收器,每次调用都会创建副本
}

// 接口实现
type Storage interface {
    QueryUser(id int) (*User, error)
}

// UserStorage实现Storage接口
var _ Storage = (*UserStorage)(nil) // 编译时检查

对于包含引用类型字段且需要共享状态的结构体,使用指针(&UserStorage{})是更合适的选择。这避免了不必要的拷贝,确保了语义的正确性,并且与Go语言中资源管理器的常见模式保持一致。

回到顶部