structstruct
struct

结构体的定义

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

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

其中:

struct_variable_typememberdefinition
Person
type person struct {
 name string
 city string
 age  int8
}

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

type person1 struct {
 name, city string
 age        int8
}
personnamecityageperson

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

结构体实例化

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

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
.
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"

结构体初始化

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

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语言中的内存对齐:

空结构体

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

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")
}
DoorDoor
type Door int
type Door bool
intbool

面试题

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
//}

//也是不可行的。

构造函数

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

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

嵌套结构体

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

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序列化

"":,
//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)

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

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

Tag
Student
//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")
    }
}