Go的结构体

Posted guotianbao

tags:

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

Go的结构体

结构体

Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct

结构体的定义

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}
  • 类型名:自定义结构体的名称,在同一个包内不能重复
  • 字段名:结构体中的字段名必须惟一
  • 字段类型:表示结构体字段的具体类型

例子一

type persion struct {
  name string
  city string
  age int8
}

结构体实例化

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

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

type persion struct {
  name string
  city string
  age int8
}

func main() {
  var p1 persion
  p1.name = "james"
  p1.city = "深圳"
  p1.age = 25
  fmt.Printf("p1=%v
", p1)
  fmt.Printf("p1=%#v
", p1)
}
// p1={james 深圳 25}
// p1=main.persion{name:"james", city:"深圳", age:25}

匿名结构体

func main() {
  var user struct{Name string; Age int}
  user.Name = "james"
  user.Age = 25
  fmt.Printf("p1=%#v
", user)
}
// user=struct { Name string; Age int }{Name:"james", Age:25}

创建指针类型的结构体

使用new关键字对结构体进行实例化,得到的是结构体的地址

type persion struct {
  name string
  city string
  age int8
}
func main() {
  var p2 =  new(persion)
  fmt.Printf("%T
", p2)
  fmt.Printf("%#v
", p2)
}
// *main.persion
// &main.persion{name:"", city:"", age:0}

p2是一个结构体指针

结构体指针可以使用.来访问结构体的成员

type persion struct {
  name string
  city string
  age int8
}
func main() {
  var p2 =  new(persion)
  p2.name = "james"
  p2.city = "深圳"
  p2.age = 25
  fmt.Println(p2.city)
  fmt.Printf("%#v
", p2)
}
// 深圳
// &main.persion{name:"james", city:"深圳", age:25}

取结构体的地址实例化

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

type persion struct {
  name string
  city string
  age int8
}
func main() {
  p3 := &persion{}         // 等于var p3 =  new(persion)
  fmt.Printf("%T
", p3)
  fmt.Printf("%#v
", p3)
  p3.name = "james"
  p3.city = "深圳"
  p3.age = 25
  fmt.Printf("%#v
", p3)
}
// *main.persion
// &main.persion{name:"", city:"", age:0}
// &main.persion{name:"james", city:"深圳", age:25}

p3.name = "james"其实在底层是(*p3).name = "james",这是Go帮我们实现的语法糖

结构体初始化

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

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

使用键值对初始化

type persion struct {
  name string
  city string
  age int8
}
func main() {
  p5 := persion {
    name : "james",
    city : "深圳",
    age : 25,
  }
  fmt.Printf("p5=%#v
", p5)
}
// p5=main.persion{name:"james", city:"深圳", age:25}

对结构体指针进行初始化

type persion struct {
  name string
  city string
  age int8
}
func main() {
  p6 := &persion {
    name : "james",
    city : "深圳",
    age : 25,
  }
  fmt.Printf("p6=%#v
", p6)
}
// p6=&main.persion{name:"james", city:"深圳", age:25}

也可以只初始化某些字段

type persion struct {
  name string
  city string
  age int8
}
func main() {
  p7 := &persion {
    name : "james",
  }
  fmt.Printf("p7=%#v
", p7)
}
// p7=&main.persion{name:"james", city:"", age:0}

结构体内存布局

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

type test struct {
  a int8
  b int8
  c int8
  d int
}
func main() {
  n := test{
    1, 2, 3, 4,
  }
  fmt.Printf("n.a: %p
", &n.a)
  fmt.Printf("n.b: %p
", &n.b)
  fmt.Printf("n.c: %p
", &n.c)
  fmt.Printf("n.d: %p
", &n.d)
}
// n.a: 0xc00006a004
// n.b: 0xc00006a005
// n.c: 0xc00006a006
// n.d: 0xc00006a007

猜结果

type student struct {
  name string
  age int
}
func main() {
  m := make(map[string]*student)
  stus := []student{
    {name: "stu1", age: 6},
    {name: "stu2", age: 7},
    {name: "stu3", age: 6},
  }
  for _, stu := range stus {
    m[stu.name] = &stu
  }
  for k, v := range m {
    fmt.Println(k, "=>", v.name)
  }
}
// stu1 => stu3
// stu2 => stu3
// stu3 => stu3
// 为什么v.name都是stu3,我也搞不懂

go的构造函数

Go语言的结构体没有构造函数,可以自己通过结构体实现

struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型

type persion struct {
  name string
  city string
  age int8
}
func newPersion(name, city string, age int8) *persion {
  return &persion{
    name : name,
    city : city,
    age : age,
  }
}
func main() {
  p9 := newPersion("james", "深圳", 26)
  fmt.Printf("%#v
", p9)
}
// &main.persion{name:"james", city:"深圳", age:26}

go的方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
  函数体
}
  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
  • 方法名、参数列表、返回参数:具体格式与函数定义相同
// Persion结构体
type Persion struct {
  name string
  age int8
}
// NewPersion构造函数
func NewPersion(name string, age int8) *Persion {
  return &Persion{
    name : name,
    age : age,
  }
}
// Dream Persion的做梦的方法
func (p Persion) Dream() {
  fmt.Printf("%s的梦想是转行厨师
", p.name)
}

func main() {
  p1 := NewPersion("james", 26)
  p1.Dream()
}
// james的梦想是转行厨师

方法与函数的区别是: 函数不属于任何类型,方法属于特定的类型

指针类型接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄

// Persion 结构体
type Persion struct {
  name string
  age int8
}
// NewPersion 构造函数
func NewPersion(name string, age int8) *Persion {
  return &Persion{
    name : name,
    age : age,
  }
}
// SetAge 设置p的年龄
// 使用指针类型接收者
func (p *Persion) SetAge(newAge int8) {
  p.age = newAge
}
func main() {
  p1 := NewPersion("james", 26)
  fmt.Println(p1.age)  // 26
  p1.SetAge(22)
  fmt.Println(p1.age)  // 22
}

值类型接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

// Persion 结构体
type Persion struct {
  name string
  age int8
}
// NewPersion 构造函数
func NewPersion(name string, age int8) *Persion {
  return &Persion{
    name : name,
    age : age,
  }
}
// Dream Persion的做梦的方法
func (p Persion) Dream() {
  fmt.Printf("%s的梦想是转行厨师
", p.name)
}
func (p Persion) SetAge2(newAge int8) {
  p.age = newAge
}
func main() {
  p1 := NewPersion("james", 27)
  p1.Dream()
  fmt.Println(p1.age)
  p1.SetAge2(30)
  fmt.Println(p1.age)
}
// james的梦想是转行厨师
// 27
// 27

什么时候应该使用指针类型接收者

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应使用指针接收者

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法

例子一

type MyInt int

func (m MyInt) SayHello() {
  fmt.Println("Hello World I am int")
}
func main() {
  var m1 MyInt
  m1.SayHello()
  m1 = 100
  fmt.Printf("%#v %T
", m1, m1)
}
// Hello World I am int
// 100 main.MyInt

注意: 不能给别的包的类型定义方法

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

// Persion 结构体Persion类型
type Persion struct {
  string
  int
}

func main() {
  p1 := Persion {
    "james",
    27,
  }
  fmt.Printf("%#v
", p1)
  fmt.Println(p1.string, p1.int)
}
// main.Persion{string:"james", int:27}
// james 27

嵌套结构体

// Address 地址结构体
type Address struct {
    Province string
    City     string
}

// User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address Address
}

func main() {
    user1 := User{
        Name:   "james",
        Gender: "boy",
        Address: Address{
            Province: "广东",
            City:    "深圳",
        },
    }
    fmt.Printf("user1=%#v
", user1)
}
// user1=main.User{Name:"james", Gender:"boy", Address:main.Address{Province:"广东", City:"深圳"}}

嵌套匿名结构体

// Address 地址结构体
type Address struct {
    Province string
    City     string
}

// User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address // 匿名结构体
}
func main() {
  var user2 User
  user2.Name = "james"
  user2.Gender = "boy"
  user2.Address.Province = "广东"    // 通过匿名结构体.字段名访问
  user2.City = "深圳"                // 直接访问匿名结构体的字段名
  fmt.Printf("user2=%#v
", user2)
}
// user2=main.User{Name:"james", Gender:"boy", Address:main.Address{Province:"广东", City:"深圳"}}

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找

注意: 嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段

结构体的"继承"

// Animal 动物
type Animal struct {
  name string
}
// Animal都有的move方法
func (a *Animal) move() {
  fmt.Printf("%v正在移动!
", a.name)
}
// Dog 动物狗
type Dog struct {
  Feet int
  *Animal //通过嵌套匿名结构体实现继承
}
// Dog 都有的wang方法
func (d *Dog) wang() {
  fmt.Printf("%v会汪汪汪~
", d.name)
}
func main() {
  d1 := &Dog{
    Feet: 5,
    Animal: &Animal{  // 注意嵌套的是结构体指针
      name: "大黄",
    },
  }
  d1.wang()
  d1.move()
}
// 大黄会汪汪汪~
// 大黄正在移动!

结构体字段的可见性

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

结构体和JSON序列化

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: "102",
    Students: make([]*Student, 0, 200),
  }
  for i := 0; i < 10; i++ {
    stu := &Student{
      ID: i,
      Name: fmt.Sprintf("stu%02d", i),
      Gender: "boy",
    }
    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
", data)
  
  // JSON反序列化: JSON格式字符串==>结构体
  str := data
  c1 := &Class{}
  err = json.Unmarshal([]byte(str), c1)
  if err != nil {
    fmt.Println("json unmarshal failed")
    return
  }
  fmt.Printf("%#v
", c1)
}

结构体的标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来

Tag在结构体字段的后方定义,由一对反引号包裹起来

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔

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

// 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: "boy",
    name: "james",
  }
  data, err := json.Marshal(s1)
  if err != nil {
    fmt.Println("json marshal failed!")
    return
  }
  fmt.Printf("%s
", data)
}
// {"id":1,"Gender":"boy"}

以上是关于Go的结构体的主要内容,如果未能解决你的问题,请参考以下文章

[Go] gocron源码阅读-go语言的结构体

GO浪---第1浪之结构体

分享几个实用的代码片段(第二弹)

分享几个实用的代码片段(第二弹)

#yyds干货盘点#愚公系列2022年08月 Go教学课程 031-结构体方法

Go语言学习之旅--结构体