Golang struct using tips

由于go语言中没有class所以struct是编程中最常用的结构,可惜的是,自己对struct的某些知识掌握的并不好,比如结构体的嵌套以及空结构体等等,项目中难免捉襟见肘,这里梳理一下。

基本内容

从面向对象的角度来看,struct中的字段代表了类型的属性,而与这个struct相关联的方法,或者说是这个struct实现了的方法,可以看成是针对这些属性的操作。

比如与java来比较,在java中,通常是声明一个class,或者继承已有的类,之后再在这个class中声明一些方法来对属性进行操作,以此来体现面向对象的特性,以及与之相关的继承和多态。

所谓结构是骨干,接口是灵魂的程序设计理念,在struct+interface的模式下,体现的是很清楚了。

在golang中,者分别是通过几个独立的机制来体现的,首先一个是struct,其中可以声明指定的字段,要是把具体的方法与这个struct相关联,则要通过method的语法来实现,就是在定义函数的时候,要明确指明,这个函数是由哪个具体的结构实现的。interface中定义了一系列的接口,struct在定义的时候,可以通过duck type的形式实现不同的interface(只要struct中相关联的方法是某个interface的超集 就说明实现了这个interface)。

interface类型的实例可以去调用具体的方法,interface中所包含的内容不仅仅是一个方法列表那么简单,具体可以看之前的一篇(golang reflection model)关于interface的介绍。之后再调用方法的时候,通过接口实例.函数名的形式来进行调用,但实际上调用的那个函数,是与对应struct关联的函数。一个接口的实例可以被赋予任何实现了这个接口中声明的方法的数据类型,在赋值的时候,接口中已经存有那个实例(就是interface中的data字段 其类型也是固定好了的)。之后实际上是调用的不同的实现,也就实现了多态。

关于继承,则是通过struct中的嵌套字段来实现的,interface与struct在定义的时候都可以进行嵌套,在struct字段嵌套的时候,又有一系列具体的使用细节。

struct中的嵌套字段

struct的声明语法很直接type newstruct struct{xxx},之后里面的字段有两种形式,一个是字段名 类型就像我们常见的len int,name string这种,另一种是直接写成类型名的形式。

这里需要注意一下作用域,当字段类型的首字母是大写的话,可以跨包通过struct类型名来访问到对应的字段,否则这些字段就是私有的,只能在该结构体所属的package内对其进行访问或者赋值。

这种直接在struct中使用类型名进行定义的形式,就是所谓的嵌套字段。

  • 名字 嵌入字段(匿名字段)的名字被隐含规定为该类型的名称,比如下面struct中四个字段的隐含名称分别是T1,T2,T3,T4。
type Anonimoustype struct{
T1
*T2
p.T3
*p.T4
}
  • 方法调用 可以在被嵌入类的实例上直接调用嵌入类的方法,这个方法会直接被转换到被嵌入类上。

  • 方法隐藏 如果被嵌入类也声明了同样的方法,则被嵌入类的方法会被隐藏,调用的时候会首先调用被嵌入类中声明的方法。这里所说的“同样”是指的方法名称相同,名称相同,但传入参数不同的话,也会被隐藏,只能通过链式调用的方式来调用嵌入结构的那个方法。

  • 字段类型 如果字段类型是非指针类型,比如自定义类型S中嵌入了T,则S中关联 了T所关联的所有方法,S中关联了 T所关联的所有方法。 如果字段类型为指针类型,比如S中嵌入 T 那么S关联了 T 或 T 的所有方法。

  • 嵌套深度可以是多层,上述覆盖原则同样适用。

下面的例子简单演示了上面几条原则:

 package main
import (
"fmt"
)
type Insertstruct struct {
s string
b int
}
func (instance Insertstruct) outputa() {
fmt.Println(instance.s)
}
func (instance Insertstruct) outputb() {
fmt.Println(instance.b)
}
type Mystruct struct {
//将Insertstruct嵌套进来
Insertstruct
myitem string
}
func (instance Mystruct) outputa() {
fmt.Println("the item of Mystruct:", instance.myitem)
}
func main() {
item := &Mystruct{Insertstruct: Insertstruct{"the type of insert", 123}, myitem: "the type of mystruct"}
//关联了嵌入结构的方法
item.outputb()
//可以看到 采用下面的表达方式输出的是同样的结果
item.Insertstruct.outputb()
//采用同名函数 输出结果不同 因为隐藏了嵌入类所关联的方法
item.outputa()
item.Insertstruct.outputa()
}
/*output:
123
123
the item of Mystruct: the type of mystruct
the type of insert
*/

匿名结构类型

匿名结构类型往往有一种临时的感觉,不需要其具有通用性,往往在定义的时候就直接初始化完成,因为没有通过type关键字来把struct{}这种类型转化成为自定义的新类型,比如

anonms:=struct{
a int
b string
}{0,"string"}

直接声明一个匿名的struct类型,把值赋给anoms变量而已。

空结构体

有的时候在程序中,会见到类似 struct{} 以及 struct{}{} 的表述,看起来比较奇怪,这里涉及两个点,一个是空结构体,另外一个是结构体的字面值。

首先明确,struct{}这种表述,是一种类型,即是结构体类型,通常的时候,我们会在里面设置好一些字段,但是有的时候,我们会直接声明一个没有字段的空结构体出来,为何?

笔试面试的时候,通常会考sizeof相关的题目,即占用内存空间的大小。或者更形象的说,width,因为内存空间是一维的,所以每个实例会在整个的栈中占一定量的width。比如下面这个例子:

package main
import (
"fmt"
"unsafe"
)
type Usera struct {
Name string
Age int
}
type Userb struct{}
type Mynulstruct struct {
A struct{}
B struct{}
}
func main() {
aa := Usera{}
ab := Usera{Name: "abc", Age: 18}
b := Userb{}
nulls := Mynulstruct{}
var s string
var n int
fmt.Println(unsafe.Sizeof(aa))
fmt.Println(unsafe.Sizeof(ab))
fmt.Println(unsafe.Sizeof(s))
fmt.Println(unsafe.Sizeof(n))
fmt.Println(unsafe.Sizeof(b))
fmt.Println(unsafe.Sizeof(nulls))
}
/*
24
24
16
8
0
0
*/

可以发现,struct{}这个空类型的实例所占的存储wodth竟然是0。也就是说,它占了0字节的存储空间,并且不需要额外的填充空间。即使像上面程序中那样,使用了嵌套的struct{},所占的字面值仍然是0。或者是声明一个很长的struct{}实例组成的sclice,所占用的内存宽度也仍然是0。比如

package main

import (
“fmt”
“unsafe”
)

type Userb struct{}

func main() {
var x [10000000]struct{}
var s = make([]struct{}, 100)
fmt.Println(unsafe.Sizeof(x))
fmt.Println(len(s))
fmt.Println(len(x))
fmt.Println(unsafe.Sizeof(s))
}

/ output
0
100
10000000
24 /

1
2
3

可以看到,直接声明的struct{}数组,没有占用任何的空间,但是用make生成的,占了额外的24byte(应该是一些元信息),但是它们的len的显示结果,都是正常的长度。
具体的使用上,一方面,空结构体可以作为方法的接受者,比如那种工具类的函数,不需要具体的字段,类似:

type Tool struct{}
func (s Tool) toola() {…}
func (s Tool) toolb() {…}
func (s *Tool) toolc() {…}

另外一种是在channel传递信号的时候,应为空结构体不占用内存,信号本身只起到一个通知的含义,这样使得程序更精简(不过感觉用int bool byte这种 在可读性上会好一点)。
还要注意的一点是初始化的操作,比如下面的例子:

package main

import (
“fmt”
)

type User struct {
Name string
Age int
}

func main() {
s := User{Name: “abc”, Age: 18}
b := User{}
fmt.Println(s)
fmt.Println(b)
}

在b实例生成的时候,直接用User{}这种方式,让其中的值为默认值,这里的User是我们提前定义的struct{Name string Age int}类型。
那么对于空结构体来说,以默认初始化的方式生成一个实例的表述就是struct{}{}这样的表述,就是一个空结构体以默认的方式生成的实例。

struct嵌套时候的初始化

比如

type Packetdetail struct {
Requestdetail string
Responddetail string
}
type HttpTransaction struct {
//insert struct
Packetdetail
Srcip string
Srcport string
Destip string
Destport string
Timesend time.Time
Timereceive time.Time
Respondtime float64
//only application layer info
}

之后初始化的时候,这样操作:

httpinstance := &metrics.HttpTransaction{
Srcip: "8082",
Srcport: "8080",
Destip: "8081",
Respondtime: 0.123456,
Packetdetail: metrics.Packetdetail{Requestdetail: "a", Responddetail: "b"},
}

注意嵌套部分的赋值操作,可以参考这个

关于struct字段之后的tag

关于struct 中tag的用法 具体就是在struct中的每个元素的后面用反引号` `加上一个串
具体的用法:

1、作为tag 标签 参看学习go语言76页 通过反射可以捕获到tag标签当中的内容 t.Elem().Field(0).Tag

2、处理xml类型的文件https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.1.md

3、处理json类型的文件https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.2.md
利用tag还可以做一些其他的操作,比如orm类的框架中,往往通过tag标记出转换到数据库之后的db的类型。

因为输出字段的名称默认都是大写的,能够被赋值的字段必须是可导出字段(即首字母大写),同时JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,要是想通过小写的方式输出 就需要采用json tag的形式
比如:

type Server struct {
ServerName string json:"serverName"
ServerIP string json:"serverIP"
}

这样不光是输出字段的时候,首字母会变成小写的,将json文件转化成结构体进行输入的时候,也是可以将key值写成小写的形式,之后转化的时候会自动赋值给首字母为大写的关键字段。

参考资料

Go并发编程实战

http://blog.csdn.net/suncaishen/article/details/9388161

http://blog.csdn.net/justaipanda/article/details/43155949

空结构体
https://www.golangtc.com/t/575442b8b09ecc02f7000057

推荐文章