Go语言中的基础数据类型可以表示一些事物的基本属性,但是要表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

结构体的定义

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

使用typestruct关键字来定义结构体,具体代码格式如下:

type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}

其中:

  • struct_variable_type:标识自定义结构体的名称,在同一个包内不能重复。
  • member:表示结构体字段名。结构体中的字段名必须唯一。
  • definition:表示结构体字段的具体类型。

定义一个Person结构体:

type person struct {
name string
city string
age int8
}

同样类型的字段可以写在一行,

type person1 struct {
name, city string
age int8
}

这样就拥有了一个person的自定义类型,它有namecityage三个字段。使用这个person结构体就能够很方便的在程序中表示和存储人的信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

结构体实例化

只有当结构体实例化时,即结构体声明后,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型

基本实例化

type Books struct {
title string
author string
subject string
book_id int
} func main() {
var book1 Books /* 声明 book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "Go大佬"
Book1.subject = "Go"
Book1.book_id = 6495407
}

通过.来访问结构体的字段(成员变量)。

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

func main() {
var user struct{Name string; Age int}
user.Name = "张三"
user.Age = 20
fmt.Printf("%#v\n", user)
}

创建指针类型结构体

还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址:

var p = new(person)
fmt.Printf("%T\n", p) //*main.person
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"", age:0}

可以看出p是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

var p = new(person)
p.name = "张三"
p.age = 18
p.city = "深圳"
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"张三", city:"深圳", age:18}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

book := &Books{}
book.title = "Java"
book.author = "Java大佬"
book.subject = "Java 语言"
book.book_id = 6495700

book.title= "Java"其实在底层是(*book).title= "Java",这是Go语言实现的语法糖。

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值。

type person struct {
name string
city string
age int8
} func main() {
var p person
fmt.Printf("p=%#v\n", p) //p=main.person{name:"", city:"", age:0}
}

使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

p := person{
name: "张三",
city: "",
age: 18,
}
fmt.Printf("p=%#v\n", p) //p=main.person{name:"张三", city:"深圳", age:18}

也可以对结构体指针进行键值对初始化,例如:

p := &person{
name: "张三",
city: "深圳",
age: 18,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"张三", city:"深圳", age:18}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

p := &person{
city: "深圳",
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"深圳", age:0}

使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

p := &person{
"张三",
"深圳",
18,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"张三", city:"深圳", age:18}

使用这种格式初始化时,需要注意:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。

结构体内存布局

结构体占用一块连续的内存。

type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)

输出:

n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063

关于Go语言中的内存对齐:在 Go 中恰到好处的内存对齐

空结构体

空结构体是不占用空间的。

var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0

空结构体的作用

因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。一是节省资源,二是空结构体本身就具备很强的语义,即这里不需要任何值,仅作为占位符。

实现集合(Set)

Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。事实上,对于集合来说,只需要 map 的键,而不需要值。即使是将值设置为 bool 类型,也会多占据 1 个字节,那假设 map 中有一百万条数据,就会浪费 1MB 的空间。

因此,将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。

type Set map[string]struct{}

func (s Set) Has(key string) bool {
_, ok := s[key]
return ok
} func (s Set) Add(key string) {
s[key] = struct{}{}
} func (s Set) Delete(key string) {
delete(s, key)
} func main() {
s := make(Set)
s.Add("Tom")
s.Add("Sam")
fmt.Println(s.Has("Tom"))
fmt.Println(s.Has("Jack"))
}

不发送数据的信道(channel)

func worker(ch chan struct{}) {
<-ch
fmt.Println("do something")
close(ch)
} func main() {
ch := make(chan struct{})
go worker(ch)
ch <- struct{}{}
}

有时候使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。这种情况下,使用空结构体作为占位符就非常合适了。

仅包含方法的结构体

type Door struct{}

func (d Door) Open() {
fmt.Println("Open the door")
} func (d Door) Close() {
fmt.Println("Close the door")
}

在部分场景下,结构体只包含方法,不包含任何的字段。例如上面的 Door,在这种情况下,Door 事实上可以用任何的数据结构替代:

type Door int
type Door bool

无论是 int 还是 bool 都会浪费额外的内存,因此这种情况下,声明为空结构体是最合适的。

面试题

type student struct {
name string
age int
} func main() {
m := make(map[string]*student)
stus := []student{
{name: "张三", age: 18},
{name: "李四", age: 23},
{name: "王五", age: 25},
} for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
} //与Java的foreach一样,for range使用的是副本的方式。
//for range在循环时,go会创建一个额外的变量去存储循环的元素,所以在每一次迭代中,该变量都会被重新赋值,
//所以m[stu.Name]=&stu实际上一致指向同一个指针,
//最终该指针的值为遍历的最后一个struct的值拷贝。 就像想修改切片元素的属性: //for _, stu := range stus {
// stu.age = stu.age+10
//} //也是不可行的。

构造函数

Go语言的结构体没有构造函数,但可以自己实现。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以构造函数返回的是结构体指针类型:

func NewPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}

调用构造函数

p := NewPerson("张三", "深圳", 18)
fmt.Printf("%#v\n", p) //&main.person{name:"张三", city:"深圳", age:18}

结构体的匿名字段

可以用字段来创建结构,这些字段只包含一个没有字段名的类型。这些字段被称为匿名字段。

在类型中,使用不写字段名的方式,使用另一个类型

type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}
func main() {
// 初始化一个学生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 访问相应的字段
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His speciality is ", mark.speciality)
// 修改对应的备注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改年龄信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改体重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}

可以使用"."的方式进行调用匿名字段中的属性值

实际就是字段的继承

其中可以将匿名字段理解为字段名和字段类型都是同一个

基于上面的理解,所以可以mark.Human = Human{"Marcus", 55, 220} mark.Human.age -= 1

若存在匿名字段中的字段与非匿名字段名字相同,则最外层的优先访问,就近原则

通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段,所有的内置类型和自定义类型都是可以作为匿名字段的。

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

type Address struct {
city, state string
}
type Person struct {
name string
age int
address Address
} func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address {
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:",p.age)
fmt.Println("City:",p.address.city)
fmt.Println("State:",p.address.state)
}

提升字段

在结构体中属于匿名结构体的字段称为提升字段,因为它们可以被访问,就好像它们属于拥有匿名结构字段的结构一样。理解这个定义是相当复杂的。

type Address struct {
city, state string
}
type Person struct {
name string
age int
Address
} func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.Address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.city) //city is promoted field
fmt.Println("State:", p.state) //state is promoted field
}

运行结果

Name: Naveen
Age: 50
City: Chicago
State: Illinois

若存在匿名字段中的字段与非匿名字段名字相同,则最外层的优先访问,就近原则

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
} //Email 邮箱结构体
type Email struct {
Account string
CreateTime string
} //User 用户结构体
type User struct {
Name string
Gender string
Address
Email
} func main() {
var user User
user.Name = "张三"
user.Gender = "男"
// user.CreateTime = "2021" //ambiguous selector user.CreateTime
user.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}

结构体的“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

//Animal 动物
type Animal struct {
name string
} func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
} //Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
} func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
} func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}

结构体字段的可见性

结构体中字段大写开头表示可公开访问(可以从其他包访问它),小写表示私有(仅在定义当前结构体的包中可访问)。

结构体与JSON序列化

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//Student 学生
type Student struct {
ID int
Gender string
Name string
} //Class 班级
type Class struct {
Title string
Students []*Student
} func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}

结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
} func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "张三",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

结构体比较

结构体是值类型,如果每个字段具有可比性,则是可比较的。如果它们对应的字段相等,则认为两个结构体变量是相等的。

type name struct {
firstName string
lastName string
} func main() {
name1 := name{"Steve", "Jobs"}
name2 := name{"Steve", "Jobs"}
if name1 == name2 {
fmt.Println("name1 and name2 are equal")
} else {
fmt.Println("name1 and name2 are not equal")
} name3 := name{firstName:"Steve", lastName:"Jobs"}
name4 := name{}
name4.firstName = "Steve"
if name3 == name4 {
fmt.Println("name3 and name4 are equal")
} else {
fmt.Println("name3 and name4 are not equal")
}
}

运行结果

name1 and name2 are equal
name3 and name4 are not equal

如果结构变量包含的字段是不可比较的,那么结构变量是不可比较的

type image struct {
data map[int]int
} func main() {
image1 := image{data: map[int]int{
0: 155,
}}
image2 := image{data: map[int]int{
0: 155,
}}
if image1 == image2 {
fmt.Println("image1 and image2 are equal")
}
}

Golang通脉之结构体的更多相关文章

  1. 将c语言的结构体定义变成对应的golang语言的结构体定义,并将golang语言结构体变量的指针传递给c语言,cast C struct to Go struct

    https://groups.google.com/forum/#!topic/golang-nuts/JkvR4dQy9t4 https://golang.org/misc/cgo/gmp/gmp. ...

  2. golang 使用匿名结构体的问题

    golang允许使用匿名结构体,形如 type Test struct { param1 struct { param2 string } } 一般在使用的时候可以直接这样初始化 a := Test{ ...

  3. 六、golang中的结构体和方法、接口

    结构体: 1.用来自定义复杂数据结构 2.struct里面可以包含多个字段(属性) 3.struct类型可以定义方法,注意和函数的区分 4.strucr类型是值类型 5.struct类型可以嵌套 6. ...

  4. golang基础--strcut结构体

    结构体struct类似python语言中的类class,结构体类的元素可以是一个变量,或者函数或者其它的类型,好比python的属性和方法. // struct结构体,类似python语言中的clas ...

  5. Golang教程:结构体

    结构体(struct)是用户自定义的类型,它代表若干字段的集合.有些时候将多个数据看做一个整体要比单独使用这些数据更有意义,这种情况下就适合使用结构体. 比如将一个员工的 firstName, las ...

  6. golang 处理 json 结构体

    首先 我们来看一下这个json 字串 { "resp": { ", "respMsg": "成功", "app" ...

  7. golang中的结构体工厂

    1. main包 package main import ( "day01/utils" "fmt" ) //type File struct { // fd ...

  8. Golang 入门 : 结构体(struct)

    Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型.试图表示一个现实世界中的实体. 结构体由一系列命名的元素组成,这些元素又被称为字段,每个字段都有一个名称和 ...

  9. 【Golang】创建有配置参数的结构体时,可选参数应该怎么传?

    写在前面的话 Golang中构建结构体的时候,需要通过可选参数方式创建,我们怎么样设计一个灵活的API来初始化结构体呢. 让我们通过如下的代码片段,一步一步说明基于可选参数模式的灵活 API 怎么设计 ...

随机推荐

  1. JDK1.8源码(八)——java.lang.ThreadLocal类

    https://www.cnblogs.com/xdd666/p/14734047.html ThreadLocal https://www.cnblogs.com/yanfei1819/p/1473 ...

  2. 手写AVL平衡二叉搜索树

    手写AVL平衡二叉搜索树 二叉搜索树的局限性 先说一下什么是二叉搜索树,二叉树每个节点只有两个节点,二叉搜索树的每个左子节点的值小于其父节点的值,每个右子节点的值大于其左子节点的值.如下图: 二叉搜索 ...

  3. docker日常使用指南

    docker日常使用指南 目录 docker日常使用指南 前言 1.基础知识 1.1 docker是什么 1.2 与虚拟机(VM)的区别 1.3 镜像与容器 2.安装 2.1 在线安装 2.2 离线安 ...

  4. 判断IE浏览器版本

    //判断IE浏览器版本 function IEVersion() { var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串 var isI ...

  5. python库--jieba(中文分词)

    import jieba 精确模式,试图将句子最精确地切开,适合文本分析:全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义:搜索引擎模式,在精确模式的基础上,对长词再次切 ...

  6. 支持Cron表达式、间隔时间的工具(TaskScheduler)

    后台任务如何支持间隔时间.Cron表达式两种方式? 分享一个项目TaskScheduler,这是我从Furion项目中拷出来的 源码:https://gitee.com/dot-net-core/ta ...

  7. 第十一章 Net 5.0 快速开发框架 YC.Boilerplate --图数据库模块Neo4j

    在线文档:http://doc.yc-l.com/#/README 在线演示地址:http://yc.yc-l.com/#/login 源码github:https://github.com/linb ...

  8. pip国内源设置

    在目录 C:\Users\Administrator下新建pip目录 C:\Users\Administrator\pip 添加 pip.ini 文件 pip.ini内容设置为 [global] in ...

  9. Set代码

    现有一整数集(允许有重复元素),初始为空.我们定义如下操作:add x 把 x 加入集合del x 把集合中所有与 x 相等的元素删除ask x 对集合中元素x的情况询问 对每种操作,我们要求进行如下 ...

  10. 在FLASH中读写结构体

    在FLASH中读写结构体 注意事项 编程(写数据)地址要对齐 写数据时,我们要指定写入的地址,如果写入地址为非对齐,则会出现编程对齐错误. 比如遵循32位(4字节)地址对齐,你的地址只能是4的倍数.0 ...