Golang中何时使用类型嵌入?

Golang中何时使用类型嵌入? 目前,我有时在DTO中会遇到需要嵌入类型(另一个DTO)的情况,就像特质那样。

4 回复

我想他指的就是这个。因此,除非是为了构建扁平结构体(不含方法),否则很少建议嵌入类型。

是的,这个观点非常好。我尤其在对那些包含公共字段的结构体进行JSON(反)序列化时这样做过。

更多关于Golang中何时使用类型嵌入?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


然而,这是我能想到的唯一做过这种情况的场景。通常我最终会忘记嵌入的函数,然后不小心做了类似这样的事情:

func (f TempFile) Read(p []byte) (n int, err error) {
    // ...
    nn, err := self.Read(p)    // 糟糕,我本意是想调用 self.File.Read...
    // ...
}

这错误相当明显,但我还是不止一次地犯了同样的错误。我想我可能就是不擅长处理这种类似继承的嵌入方式 😵。

这让我想起了 Bitfield 博客中的一句话,出自这篇文章 https://bitfieldconsulting.com/golang/commandments:

不要嵌入结构体类型,让它们神奇地获得不可见的方法。

我想他指的就是这种情况。因此,除非是为了构建扁平的结构体(没有方法),否则很少建议嵌入类型。

如果你熟悉其他支持继承的语言(如Java、Python、C++、C#等)。例如,我在一些包中有一个简单的TempFile实现,本质上如下所示:

type TempFile struct {
    *os.File
}

func (f TempFile) Close() error {
    if err := f.try("close", func(f TempFile) error {
        return f.File.Close()
    }); err != nil {
        return err
    }
    return f.try("remove", func(f TempFile) error {
        return os.Remove(f.File.Name())
    })
}

func (f TempFile) try(fn func(TempFile) error) error {
    if err := fn(f); err != nil {
        return fmt.Errorf(
            "error attempting to %s file %v: %w",
            what, f.Name(), err,
        )
    }
    return nil
}

因此,本质上我的TempFile类型与*os.File的工作方式相同,只是在调用Close时,它会调用“基类”*os.FileClose函数,然后删除文件。如果*os.File将来获得新方法,我的TempFile将“继承”它。

然而,这是我唯一能想到的使用了这种模式的情况。通常我最终会忘记嵌入的函数,然后不小心写出这样的代码:

func (f TempFile) Read(p []byte) (n int, err error) {
    // ...
    nn, err := self.Read(p)    // 糟糕,我本意是调用 self.File.Read...
    // ...
}

这错误相当明显,但我却不止一次地犯同样的错误。我想我可能不太适合这种类似继承的嵌入方式。upside_down_face

在Golang中,类型嵌入通常用于以下场景:

  1. 代码复用:当多个结构体需要共享相同的字段和方法时
  2. 接口组合:通过嵌入接口来扩展接口功能
  3. 模拟继承:虽然Go没有传统继承,但嵌入可以实现类似效果

对于DTO场景,类型嵌入特别有用:

// 基础DTO
type BaseDTO struct {
    ID        int       `json:"id"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// 用户DTO嵌入基础DTO
type UserDTO struct {
    BaseDTO           // 嵌入类型
    Username  string  `json:"username"`
    Email     string  `json:"email"`
}

// 产品DTO也嵌入相同的基础DTO
type ProductDTO struct {
    BaseDTO           // 嵌入类型
    Name      string  `json:"name"`
    Price     float64 `json:"price"`
}

func main() {
    user := UserDTO{
        BaseDTO: BaseDTO{
            ID:        1,
            CreatedAt: time.Now(),
            UpdatedAt: time.Now(),
        },
        Username: "john_doe",
        Email:    "john@example.com",
    }
    
    // 可以直接访问嵌入字段
    fmt.Printf("User ID: %d\n", user.ID)
    fmt.Printf("Username: %s\n", user.Username)
}

类型嵌入也常用于接口组合:

type Reader interface {
    Read() string
}

type Writer interface {
    Write(string)
}

// 通过嵌入组合接口
type ReadWriter interface {
    Reader
    Writer
}

type File struct {
    content string
}

func (f *File) Read() string {
    return f.content
}

func (f *File) Write(s string) {
    f.content = s
}

func process(rw ReadWriter) {
    rw.Write("data")
    fmt.Println(rw.Read())
}

在DTO中使用类型嵌入时需要注意:

  • 嵌入类型的方法会提升到外层结构体
  • 字段访问是透明的,可以直接访问
  • 如果有字段名冲突,需要显式指定嵌入类型名
  • JSON序列化时,嵌入字段会平铺展开
回到顶部