理解go中空结构体的应用和实现原理

Posted Go语言中文网

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解go中空结构体的应用和实现原理相关的知识,希望对你有一定的参考价值。

= <s s ( a := b := := emptyStruct fmt. fmt. fmt. fmt. fmt.zerobase gcphase == _GCmarktermination size == ...CanSkipFuncs = tm.stopc <- <-tm.donec: <-tm.doneclimit = _, w := work limit <- w() <-limit () // …………
04 总结
空结构体是一种不包含任何字段的结构体类型,不仅具有结构体类型的一切属性,而且该结构体类型占用的空间为0。常被用于map的集合或和通道配合使用发送信号使用的场景。

参考链接:

https://blog.haohtml.com/archives/20339

https://ijayer.github.io/post/tech/code/golang/20200419_emtpy_struct_in_go/


推荐阅读

  • Golang最细节篇— struct 空结构体究竟是啥?

  • 福利

    我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

    Go语言结构体

    文章目录

    一、Go语言结构体

    • 基础概念

      • Go语言之中结构体的实例化,使用new或者&构造的类型实例为结构体指针

      • Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法

    • 语法糖

      • Go语言让我们可以像访问普通结构体一样使用.访问结构体指针的成员

    二、结构体的实例化

    • 在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作

      ins := &T //初始化内容为空
      
      //T 表示结构体类型
      //ins 为结构体的实例,类型为 *T,是指针类型
      
    • 使用new的格式进行实例化

      ins := new(T) //ins为T类型的指针
      
    • 基本实例

      var ins T
      

    三、初始化结构体成员方法

    • 使用“键值对”初始化结构体,可以初始化一部分成员

      • 键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中
      • 结构体实例化后字段的默认值是字段类型的默认值,例如 ,数值为 0、字符串为 “”(空字符串)、布尔为 false、指针为 nil 等
      //基本格式
      ins := 结构体类型名
          字段1: 字段1的值,
          字段2: 字段2的值,
      
      //实例演示
      type Cat struct 
      	name  string
      	color string
      
      
      func main() 
      	cat := Catname: "white"
      	fmt.Println(cat) //white 
      
      
      
    • 使用多个值的列表初始化结构体:必须初始化结构体的所有字段、顺序一致、不能和键值对方式混用

      //基本格式
      ins := 结构体类型名
          字段1的值,
          字段2的值,
      
      //实例演示
      type Cat struct 
      	name  string
      	color string
      
      
      func main() 
      	cat := Cat"helloCat", "white"
      	fmt.Println(cat) //helloCat white
      
      
    • 初始化匿名结构体

      • 匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成

        ins := struct 
            // 匿名结构体字段定义
            字段1 字段类型1
            字段2 字段类型2
            // 字段值初始化
            初始化字段1: 字段1的值,
            初始化字段2: 字段2的值,
        
      • 键值部分是可以选择的,不初始化成员时,格式可以变为

        ins := struct 
            字段1 字段类型1
            字段2 字段类型2
        
      • 使用演示

        func printStruct(msg *struct 
        	name string
        	id   int
        ) 
        	fmt.Printf("%T\\n", msg) //打印msg的类型 *struct  name string; id int 
        
        
        func main() 
        	printStruct(&struct 
        		name string
        		id   int
        	
        		"jim",
        		124,
        	)
        
        

    四、模拟构造函数

    • Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数

    • 实际上就是定义一个函数,通过传入的参数进行结构体对象的构造

      func NewCat(name, color string) Cat 
      	return Catname, color
      
      
      func main() 
      
      	cat := NewCat("hellocat", "white")
      	fmt.Println(cat) //hellocat white
      
      

    五、Go语言方法和接收器

    5.1基础概念

    • 接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型

    • 类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的

    5.2为结构体添加方法

    接收器的格式如下

    func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) 
        函数体
    
    

    指针类型的接收器由一个结构体的指针组成,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的

    当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

    type Base struct 
    	name string
    	id   int
    
    
    //指针接收器
    func (b *Base) ptrBase(name string, id int) 
    	b.id = id
    	b.name = name
    
    
    //非指针接收器
    func (b Base) objBase(name string, id int) 
    	b.id = id
    	b.name = name
    
    
    func main() 
    	var b Base //声明对象
    	b.objBase("123", 123)
    	fmt.Println(b) // 0 -> 非指针接收器的修改不会被保存
    
    	b.ptrBase("456", 456)
    	fmt.Println(b) // 456 456  -> 指针接收器的修改被保存了
    
    

    六、为任意类型添加方法

    • 为基础类型添加方法

      //重定义基础类型
      type myint int
      
      //实现方法
      func (value myint)IsZero() bool 
      	return value==0
      
      
      func main()
      	var a myint =100
      	var b myint
      
      	fmt.Println(a.IsZero()) //false
      	fmt.Println(b.IsZero()) //true
      
      

    七、Go语言使用事件系统实现事件的响应和处理

    7.1 基础概念

    • Go语言可以将类型的方法与普通函数视为一个概念,从而简化方法和函数混合作为回调类型时的复杂性

    7.2 方法和函数的同一调用

    • 示例:定义一个结构体方法和一个普通函数,使它们的参数完全一致,也就是方法和函数的签名一致

      package main
      
      import "fmt"
      
      // 声明一个结构体
      type class struct 
      
      
      // 给结构体添加Do方法
      func (c *class) Do(v int) 
          fmt.Println("call method do:", v)
      
      
      // 普通函数的Do
      func funcDo(v int) 
          fmt.Println("call function do:", v)
      
      
      func main() 
          // 声明一个函数回调
          var delegate func(int)
      
          // 创建结构体实例
          c := new(class)
      
          // 将回调设为c的Do方法
          delegate = c.Do
          // 调用
          delegate(100) //call method do: 100
      
          // 将回调设为普通函数
          delegate = funcDo
          // 调用
          delegate(100) //call function do: 100
      
      • 由代码输出结果可知:无是普通函数还是结构体方法,只要签名(参数、返回值,也就是c++中的函数声明)一致,与它们签名一致的函数变量就可以保存普通函数或是结构体方法

    7.3 事件系统的基本原理

    • 事件系统可以将事件派发者与事件处理者解耦

    • 比如,网络底层可以生成各种事件,在网络连接之后,网络底层只需要将事件派发出去,而不需要关心到底哪些代码是用来响应连接上的逻辑。就如同微博,你关注了明星,明星的微博消息会通知你,但是他们并关注粉丝看到消息后的反应

    • 事件系统基本原理图

      • 一个事件系统拥有的特性
        • 能够实现事件的一方,可以根据事件 ID 或名字注册对应的事件
        • 事件发起者,会根据注册信息通知这些注册者
        • 一个事件可以有多个实现方响应

    7.4事件的注册、调用、使用事件系统

    • 事件的注册:事件系统需要为外部提供一个注册入口。这个注册入口传入注册的事件名称和对应事件名称的响应函数事件注册的过程就是将事件名称和响应函数关联并保存起来;事件注册方通过事件系统注册应该响应哪些事件及如何使用回调函数处理这些事件

    • 事件的调用:事件调用方和注册方是事件处理中完全不同的两个角色。事件调用方是事发现场,负责将事件和事件发生的参数通过事件系统派发出去,而不关心事件到底由谁处理

    • 使用事件系统

      package main
      
      import "fmt"
      
      // 实例化一个通过字符串映射函数切片的map
      var eventByName = make(map[string][]func(interface))
      
      // 注册事件,提供事件名和回调函数
      func RegisterEvent(name string, callback func(interface)) 
      
          // 通过名字查找事件列表 -> 通过名字得到函数列表切片。第一次注册时,得到的是空的列表切片
          list := eventByName[name]
      
          // 在列表切片中添加函数 -> 将传入的函数添加到对应的名字切片之中
          list = append(list, callback)
      
          // 将修改的事件列表切片保存回去
          eventByName[name] = list
      
      
      // 调用事件
      func CallEvent(name string, param interface) 
      
          // 通过名字找到事件列表
          list := eventByName[name]
      
          // 遍历这个事件的所有回调
          for _, callback := range list 
      
              // 传入参数调用回调
              callback(param)
          
      
      
      func main() 
      	//注册事件 
      	RegisterEvent("test",func(event interface) fmt.Println(event))
      	RegisterEvent("test",func(event interface) fmt.Println("second register:",event))
      
      	//调用事件
      	CallEvent("test","i am here!!")
      
      	//运行结果
      	// i am here!!
      	// second register: i am here!!
      
      
      • 一般来说,事件系统不保证同一个事件实现方多个函数列表中的调用顺序,事件系统认为所有实现函数都是平等的

    八、类型内嵌和结构体内嵌

    8.1 概念

    • 结构体可以包含一个或者多个匿名(内嵌)字段,即这些字段没有显示的名字,只有字段的类型是必须的,因此在结构体之中,对于每一种数据类型只能有一个匿名字段

    • 匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体

    • 可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐

      package main 
      import "fmt"
      
      type Base struct 
      	value1 int
      	value2 int
      
      
      type Child struct 
      	v1 int
      	v2 int 
      	//匿名字段
      	int 
      	Base 
      
      
      func main() 
      	child := new (Child)
      	child.v1=100
      	child.v2=200
      	child.int=300 //用类型代替名称
      	child.value1=400
      	child.value2=500
      	fmt.Println(child) //&100 200 300 400 500
      
      	//换一种赋值的方式
      	child2 := &Child1000,2000,3000,Base4000,5000
      	fmt.Println(child2) //&1000 2000 3000 4000 5000
      
      

    8.2 内嵌结构体

    • 如上示例,外层结构体通过 child.value直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现
    • 结构体内嵌特性
      • 内嵌的结构体可以直接访问其成员变量
        • 嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c
      • 内嵌结构体的字段名是它的类型名
        • 内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名
        • 无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错

    8.3 使用内嵌结构体解析JSON格式的数据

    package main
    import (
    "encoding/json"
    "fmt"
    )
    //1、定义数据结构
    
    // 定义手机屏幕
    type Screen struct 
    Size float32 // 屏幕尺寸
    ResX, ResY int // 屏幕水平和垂直分辨率
    
    // 定义电池
    type Battery struct 
    Capacity int // 容量
    
    
    //准备Json数据,准备手机数据结构,填充数据,将数据序列化为 JSON 格式的字节数组
    
    // 生成json数据
    func genJsonData() []byte 
    // 完整数据结构
    raw := &struct 
    Screen //手机屏幕
    Battery //电池
    HasTouchID bool // 序列化时添加的字段:是否有指纹识别
    
    // 屏幕参数
    Screen: Screen
    Size: 5.5,
    ResX: 1920,
    ResY: 1080,
    ,
    // 电池参数
    Battery: Battery
    2910,
    ,
    // 是否有指纹识别
    HasTouchID: true,
    
    
    // 将数据序列化为json
    jsonData, _ := json.Marshal(raw)//raw之中包含了参数结构体,这样序列化一次即可
    return jsonData //返回json数据
    
    
    //分离JSON数据
    
    func main() 
    // 生成一段json数据
    jsonData := genJsonData() //拿到json数据
    
    fmt.Println(string(jsonData))//打印json数据 "Size":5.5,"ResX":1920,"ResY":1080,"Capacity":2910,"HasTouchID":true
    
    // 只需要屏幕和指纹识别信息的结构和实例
    screenAndTouch := struct 
    Screen
    HasTouchID bool
    
    // 反序列化到screenAndTouch
    json.Unmarshal(jsonData, &screenAndTouch)
    // 输出screenAndTouch的详细结构
    fmt.Printf("%+v\\n", screenAndTouch) //打印结构体数据 Screen:Size:5.5 ResX:1920 ResY:1080 HasTouchID:true
    
    
    // 只需要电池和指纹识别信息的结构和实例
    batteryAndTouch := struct 
    Battery
    HasTouchID bool
    
    // 反序列化到batteryAndTouch
    json.Unmarshal(jsonData, &batteryAndTouch)
    // 输出screenAndTouch的详细结构
    fmt.Printf("%+v\\n", batteryAndTouch) //打印结构体数据Battery:Capacity:2910 HasTouchID:true
    
    
    • 示例之中可以看到,通过匿名结构体来进行参数的设置和获取,和c结构体嵌套类似

    九、Go语言垃圾回收和SetFinalizer

    9.1 垃圾回收机制(GC)基础概念

    • Go语言自带垃圾回收机制(GC)。GC 通过独立的进程执行,它会搜索不再使用的变量,并将其释放。需要注意的是,GC 在运行时会占用机器资源
    • GC 是自动进行的,如果要手动进行 GC,可以使用 runtime.GC() 函数,显式的执行 GC。显式的进行 GC 只在某些特殊的情况下才有用,比如当内存资源不足时调用 runtime.GC() ,这样会立即释放一大片内存,但是会造成程序短时间的性能下降

    9.2 finalizer(终止器)基础概念

    • finalizer(终止器)是与对象关联的一个函数,通过 runtime.SetFinalizer 来设置,如果某个对象定义了 finalizer,当它被 GC 时候,这个 finalizer 就会被调用,以完成一些特定的任务,例如发信号或者写日志等

    • Go语言中 SetFinalizer 函数定义如下

      func SetFinalizer(x, f interface)
      
      • 参数说明

        • 参数 x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针
        • 参数 f 必须是一个函数,它接受单个可以直接用 x 类型值赋值的参数,也可以有任意个被忽略的返回值
      • 执行流程

        • SetFinalizer 函数可以将 x 的终止器设置为 f,当垃圾收集器发现 x 不能再直接或间接访问时,它会清理 x 并调用 f(x)
        • 不保证终止器会在程序退出前执行,因此一般终止器只用于在长期运行的程序中释放关联到某对象的非内存资源。例如,当一个程序丢弃一个 os.File 对象时没有调用其 Close 方法,该 os.File 对象可以使用终止器去关闭对应的操作系统文件描述符
        • 终止器会按依赖顺序执行:如果 A 指向 B,两者都有终止器,且 A 和 B 没有其它关联,那么只有 A 的终止器执行完成,并且 A 被释放后,B 的终止器才可以执行
        • 如果 *x 的大小为 0 字节,也不保证终止器会执行
        • 我们也可以使用SetFinalizer(x, nil)来清理绑定到 x 上的终止器
        • 终止器只有在对象被 GC 时,才会被执行。其他情况下,都不会被执行,即使程序正常结束或者发生错误
      • 示例

        package main
        
        import (
            "log"
            "runtime"
            "time"
        )
        
        type Road int
        
        func findRoad(r *Road) 
            log.Println("road:", *r)
        
        
        func entry() 
            var rd Road = Road(999)
            r := &rd
        
        	//设置终止其,参数为int*,终止其为打印参数的地址
            runtime.SetFinalizer(r, findRoad)
        
        
        func main() 
        
            entry() //进行调用
        
            for i := 0; i < 10; i++ 
                time.Sleep(time.Second)//进行休眠
                runtime.GC()//手动GC
            
        
        
        // 2022/02/09 22:47:00 road: 999 十次循环只打印一次,说明当x被GC时,才会调用
        

    十、将结构体转换为JSON数据

    10.1 为什么这么做

    • JSON是当前互联网最常用的信息交换格式之一,而Go语言标准库提供了编码和解码JSON的包。使用"encoding/json"可轻松将结构体转换为JSON格式

    10.2 示例

    • 普通示例

      package main
      
      import (
      	"encoding/json"
      	"fmt"
      )
      
      type base struct 
      	Name string
      	Age  int
      
      
      func main() 
      	b := base
      		Name: "Jim",
      		Age:  50,
      	
      	fmt.Println(b) //Jim 50
      
      	jsonData, err := json.Marshal(b) //转换为json格式
      	if err == nil 
      		stringData := string(jsonData) //转换成字符串
      		fmt.Println(stringData)        //"Name":"Jim","Age":50
      	
      
      
      
    • 添加标签

      • 上述得到的json数据为驼峰形式,添加标签

        type base struct 
        	Name string	`json:"name"`
        	Age  int	`json:"age"`
        
        
        Jim 50
        "name":"Jim","age":50 //小写形式
        
      • 添加 omitempty标签忽略空值

        package main
        
        import (
        	"encoding/json"
        	"fmt"
        )
        
        type base struct 
        	Name string	`json:"name"`
        	Age  int	`json:"age"`
        
        
        func main() 
        	b := base
        		Name: "Jim",
        	
        	fmt.Println(b) //Jim 0
        
        	jsonData, err := json.Marshal(b) //转换为json格式
        	if err == nil 
        		stringData := string(jsonData) //转换成字符串
        		fmt.Println(stringData)        //"name":"Jim","age":0
        	
        
        
        //添加后
        type base struct 
        	Name string	`json:"name"`
        	Age  int	`json:"age,omitempty"`
        
        
        Jim 0
        "name":"Jim" //可以看到没有输出age字段
        

    十一、链表

    11.1 基本概念

    • 和C ++ 之中的链表是一样的,是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的

    11.2 单向链表

    • 单向链表的三个概念

      • 首元结点:就是链表中存储第一个元素的结点,如下图中 a1 的位置
      • 头结点:它是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以存储链表的长度或者其它的信息,也可以为空不存储任何信息
      • 头指针:它是指向链表中第一个结点的指针。若链表中有头结点,则头指针指向头结点;若链表中没有头结点,则头指针指向首元结点
    • 头节点不是必须的,头结点的好处

      • 首元结点的地址保存在头结点的指针域中,对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理
      • 无论链表是否为空,头指针都是指向头结点的非空指针,若链表为空的话,那么头结点的指针域为空
    • 使用struct定义单链表

      package main
      
      import "fmt"
      
      type list struct 
      	data int
      	next *list
      
      
      func Print(p *list) 
      	for p != nil 
      		fmt.Println(*p)
      		p = p.next
      	
      
      
      func main() 
      	head := &list1, nil
      	node1 := &list2, nil
      	node2 := &list3, nil
      	head.next = node1
      	node1.next = node2
      	Print(head)
      
      //运行结果
      1 0xc00010c210
      2 0xc00010c220
      3 <nil>
      
    • 插入节点