Go语言必知必会100问题-10 小心类型嵌入导致的问题

小心类型嵌入导致的问题

在定义结构体时,Go语言支持通过类型嵌入的形式定义结构体字段。但是,如果我们没有真正理解类型嵌入的意义,有时可能会导致意想不到的行为。本文将主要分析如何嵌入类型,类型嵌入的作用以及可能出现的问题。

在Go语言中,如果定义的结构体字段没有名称,则称该字段为嵌入字段。例如下面结构体Foo中的Bar是嵌入的。因为Bar类型被声明没有关联名称,所以它是一个嵌入字段。

type Foo struct {Bar
}
type Bar struct {Baz int
}

嵌入可以用来提升嵌入类型的字段和方法,像上面的代码,由于Bar包含一个Baz字段,它被提升到Foo中,就好像Foo中定义了一个Baz字段一样。

在这里插入图片描述

因此,可以通过Foo直接访问Baz字段。

foo := Foo{}
foo.Baz = 42

注意,Baz可从两个不同的路径获得。既可以使用Foo.Baz,也可以通过Bar采用Foo.Bar.Baz获得,两种方式获取的效果是等价的。

NOTE: 本文仅讨论结构体中字段嵌入问题。嵌入也可以用于接口,一个接口内部可以嵌入其他接口。例如,io.ReadWriter由一个io.Reader和一个io.Writer组成。

type ReadWriter interface {ReaderWriter
}

前面我们已分析了什么是类型嵌入,下面来看一个错误使用类型嵌入的例子。该例子将实现一个结构体保存一些内存中的数据,并且通过锁保护对它的并发访问。

type InMem struct {sync.Mutexm map[string]int
}
func New() *InMem {return &InMem{m: make(map[string]int)}
}

将结构体 InMem 中的map m定为小写,限制调用方直接操作m, 而是通过导出的方法进行操作。互斥锁以内嵌的形式存在(sync.Mutex), 获取结构体中数据的Get方法实现如下:

func (i *InMem) Get(key string) (int, bool) {i.Lock()v, contains := i.m[key]i.Unlock()return v, contains
}

由于互斥锁是嵌入的,我们可以直接从接收器i访问Lock和Unlock方法。前面说了这是一个错误的例子,错误在什么地方呢?由于sync.Mutex是嵌入类型,Lock和Unlock方法将被提升。因此,使用InMem的调用方也可以看到这两个方法. 这种由于内嵌导致的方法提升可能不是我们希望的,在大多数情况下,互斥锁是我们希望封装在结构体内部并且使外部客户端不可见的内容。因此,在这种情况下,不应该将其设置为嵌入字段。

m := inmem.New()
m.Lock() // ??

而应该是这样,调整为非嵌入字段。由于mu不可导出,它不能被外部客户端直接调用。

type InMem struct {mu sync.Mutexm map[string]int
}

现在来看另外一个例子,这次使用嵌入类型是一种正确的做法。例子描述的是实现一个自定义的日志记录功能,它包含一个io.WriteCloser 并对外暴露Write和Close两个方法。如果io.WriteCloser不是嵌入的,需要下面这样编码。

type Logger struct {writeCloser io.WriteCloser
}
func (l Logger) Write(p []byte) (int, error) {return l.writeCloser.Write(p)
}
func (l Logger) Close() error {return l.writeCloser.Close()
}
func main() {l := Logger{writeCloser: os.Stdout}_, _ = l.Write([]byte("foo"))_ = l.Close()
}

Logger必须提供一个Write和Close方法,然后调用writeCloser进行真正的Write和Close. 但是,如果将字段改为内嵌,就不用为Logger创建Write和Close方法,实现代码如下。

type Logger struct {io.WriteCloser
}
func main() {l := Logger{WriteCloser: os.Stdout}_, _ = l.Write([]byte("foo"))_ = l.Close()
}

从客户端角度看,调用方式没有任何差别,都是调用Write和Close方法。但是采用内嵌不用对Logger再做一层包装,Logger继承了io.WriteCloser的方法,所以Logger满足了io.WriteCloser接口。

NOTE:嵌入和OOP子类化,有人对嵌入和OOP子类化区分不清楚,这两者之间的主要区别是方法的接收者不同。下图左侧表示类型X嵌入在类型Y中,而右侧类型Y继承类型X。通过嵌入,Foo的接收者仍然是X而不是Y. 但是通过子类化,Foo的接收者是Y而不是X。Go中的嵌入是组合关系,而不是继承关系。详细分析参考(http://sd.jtimothyking.com/2018/06/25/subclassing-vs-embedding-in-golang/)

在这里插入图片描述

总结,对于类型嵌入,我们需要知道它不是必须的,这意味着无论什么时候,我们都可以在不使用类型嵌入的情况下解决问题。使用类型嵌入大多数情况下是为代码编写方便。如果决定使用类型嵌入,我们需要牢记下面两个原则:

  • 类型嵌入不应该仅用作一些语法糖来简化对字段的访问(例如调用Foo.Baz()而不是调用Foo.Bar.Baz()),如果这是唯一的目的,不用使用类型嵌入。

  • 类型嵌入不应该提升我们想要对外部隐藏的数据字段和行为方法。例如,像本文的例子,允许调用方访问应该对结构体保持私有的互斥锁。

NOTE:有些人会说,在可导出的结构体上使用类型嵌入可能会导致代码在维护方面需要付出额外的功夫。的确,将类型嵌入到导出的结构体中意味着在这种类型演变时保持小心。例如,如果我们向内部类型添加一个新方法,应该确保它不会破坏上面的第二个原则。因此,为了避免这种额外的工作,开发人员要防止将类型嵌入到公共结构体中。

牢记上述使用类型嵌入的原则,合理地使用类型嵌入可以帮助我们避免带有额外转发方法的代码(像上面的Logger中类型嵌入)。但是,不要为了用而用,不要将隐藏数据字段和方法约束丢给调用方,使用时要有充分的理由。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/499260.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

生成voc格式数据集

数据集存放格式:(Annotations文件夹放标注的xml文件,JPEGImages文件夹放标注的图片) 运行代码: import os import random import xml.etree.ElementTree as ETimport numpy as npdef get_classes(classes_path):with …

【LeetCode:124. 二叉树中的最大路径和 + 二叉树+递归】

🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…

印象笔记 - Markdown 入门指南

一、Markdown 是什么? Markdown 是一种轻量级的「标记语言」,创始人为约翰格鲁伯,用简洁的语法代替排版,目前被越来越多的知识工作者、写作爱好者、程序员或研究员广泛使用。其常用的标记符号不超过十个,相对于更为复…

2024年腾讯云优惠政策_腾讯云TOP10优惠活动

腾讯云服务器多少钱一年?62元一年起,2核2G3M配置,腾讯云2核4G5M轻量应用服务器218元一年、756元3年,4核16G12M服务器32元1个月、312元一年,8核32G22M服务器115元1个月、345元3个月,腾讯云服务器网txyfwq.co…

前端架构: 脚手架之包管理工具的案例对比及workspaces特性的使用与发布过程

npm的workspaces 特性 1 )使用或不使用包管理工具的对比 vue-cli 这个脚手架使用 Lerna 管理,它的项目显得非常清晰在 vue-cli 中包含很多 package 点开进去,每一个包都有package.json它里面有很多项目,再没有 Lerna 之前去维护和…

代码随想录算法训练营第四天

● 自己看到题目的第一想法 24.两两交换链表中的节点 方法:虚拟头节点 思路: 设置虚拟头节点dummyhead 设置临时指针cur dummyhead; cur每次向前移动两步 循环条件: cur ! nullptr && cur->next ! nullptr && cur->…

ntp时钟服务安装- 局域网节点时间同步

场景: 一般部署大数据相关应用服务,各个节点之间需要时间同步;内网情况下,很可能各节点之前时间可能不一致,或者过一段时间后 又不一致了 ntp 时钟服务器: 可用于内网各个节点之前得时间同步,安…

MATLAB环境下使用相关图可视化相关矩阵

为了处理各行各业中出现的高维数据,迫切需要寻找适用的统计学方法。大维随机矩阵理论是处理高维数据的理论工具之一,在高维统计分析中,表现出良好的性能并有着广泛的应用。 二十世纪四十年代和五十年代初期,大维随机矩阵理论起源…

前端,测试,后端,该如何选择?

前端开发,测试,后端,该如何选择?说实话,只要对互联网行业有了解的,都会推荐你学测试。 首先必须声明,能在前端开发、测试、后端(主要是Java)这三个岗位中进行选择&#…

【Intel oneAPI实战】使用英特尔套件解决杂草-农作物检测分类的视觉问题

目录 一、简介:计算机视觉挑战——检测并清除杂草二、基于YOLO的杂草-农作物检测分类2.1、YOLO简介2.2、基于YOLO的杂草-农作物检测分类解决方案 三、基于YOLO的杂草-农作物检测分类系统设计3.1、基于flask框架的demo应用程序后端3.2、基于Vue框架的demo应用程序前端…

Unity 使用脚本获取组件,代码生成预制体

代码获取组件 using System; using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine;// 必须要继承 MonoBehaviour 才是一个组件 // 类名必要与文件名一致public class c1 : MonoBehaviour {// 使用 public 初始变量时…

day09_面向对象_构造方法_封装

今日内容 零、 复习昨日 一、构造方法 二、重载 三、封装 零、 复习昨日 1 类和对象是什么关系? 类是模板(原材料)对象是具体实例(成品)类创建出对象 2 类中有什么?(类的成员) 成员属性(成员变量), 成员方法 3 创建对象的语法? 类名 对象名 new 类名(); 4 调用对象属性,方法…