深入理解Go语言:复合类型之结构体

silverwq
2022-07-12 / 0 评论 / 274 阅读 / 正在检测是否收录...

前言

Go 语言中,没有类的概念,使用结构实现类似类的功能。

语言内置的基础数据类型是用来描述一个值,而结构体是用来描述一组值。

数组、切片和 Map 可以用来表示 同一种数据类型 的集合,但是当我们要表示 不同数据类型 的集合时就需要用到结构体。

定义结构体

使用 typestruct 关键字来定义结构体

type 结构体名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}
// 例如
type person struct {
    name string
 city string
 age  int8
}

结构体名:表示结构体的名称,在同一个包内不能重复,并且最好不要以包的名称开头,不然 idea 会警告

字段名:表示结构体字段名,结构体中的字段名必须唯一

字段类型:表示结构体字段的具体类型

通常结构体中一个字段占一行,但是类型相同的字段,也可以放在同一行,同样类型的字段也可以写在一行

type person struct {
    name, city string // name, city是同一个类型
    age        int8
}

一个结构体中的字段名是唯一的,例如一下代码,出现了两个 Name 字段,是错误的:

type Student struct {
    Name string
 Name string
}

结构体还可以省略字段名称,这种字段叫:结构体匿名字段。匿名结构体字段,默认会采用类型名作为字段名;由于结构体要求字段名称必须唯一,所以结构体中同种类型的匿名字段只能有一个,不然就会重复。

type person struct {
   string // 匿名字段
  int8   // 匿名字段
}

func main() {
    p1 := person{
       string: "张三",
     int8:   18,
 }
   fmt.Println(p1.string)
  fmt.Println(p1.int8)
}

结构体还可以定义字段的可见性,字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问),那么其他 package 就无法直接使用该字段

// 在包 pk1 中定义 Student 结构体
package pk1
type Student struct{
    Age  int
    name string
}
// 在另外一个包 pk2 中调用 Student 结构体
package pk2

func main(){
    stu := Student{}
    stu.Age = 18        //正确
    stu.name = "name"  // 错误,因为`name` 字段为小写字母开头,不对外暴露
}

对结构体 json 化的时候,私有的字段将会丢失

type Student struct {
  Id   int
    Name string
 age  int // 私有
}

func main() {
    studentInfo := Student{
     Id:   1,
        Name: "小明",
       age:  35,
   }
   // json 格式化
 data, err := json.Marshal(studentInfo)
  if err != nil {
     fmt.Println("json encode 错误")
     return
  }
   fmt.Printf("%s", data) // {"Id":1,"Name":"小明"},年龄字段已经丢失了
}

这样对结构体进行 json 的时候,默认输出字段的名称就是结构体字段的名称,我们经常需要修改,不希望输出的字段名称是大写字母开头的,这个时候就需要 结构体标签 的功能了,结构体标签可以是多个,用空格隔开

type Student struct {
 Id   int    `json:"id"`
   Name string `json:"name"`
 age  int    // 私有
}

func main() {
 studentInfo := Student{
     Id:   1,
        Name: "小明",
       age:  35,
   }
   // json 格式化
 data, err := json.Marshal(studentInfo)
  if err != nil {
     fmt.Println("json encode 错误")
     return
  }
   fmt.Printf("%s", data) // {"id":1,"name":"小明"},这个时候字段的名称就是小写的了
}

结构体初始化

声明结构体的类型的变量的时候进行初始化,这个时候结构体的变量每个字段都有它默认类型的零值,这个和切片、map 不一样,切片和 map 定义之后,需要初始化才能使用

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

结构体是通过 {} 括号进行初始化的

func main() {
   p1 := person{}
  fmt.Printf("%#v\n", p1) // 输出main.person{name:"", city:"", age:0}
    p2 := person{
       name: "张三",
       city: "厦门",
       age:  30, //最后一个逗号不能忘记
  }
   fmt.Printf("%#v\n", p2) // 输出main.person{name:"张三", city:"厦门", age:30}
}

可以对结构体的部分字段进行初始化,其它字段的值是默认类型的零值

type person struct {
    name, city string
    age        int8
}
func main () {
   p:= &person{
       name:"张三",
       age: 30,
   }
   fmt.Printf("%#v",p)
}

还可以使用值列表对结构体进行初始化,不需要写 key,不过有几个约束条件:

  1. 必须初始化结构体的 所有字段
  2. 初始值的填充顺序必须与字段在结构体中的声明 顺序一致
  3. 该方式不能和键值初始化方式 混用
type person struct {
    name, city string
    age        int8
}
func main () {
   p := person{
       "张三",
       "厦门",
       30,
   }
   fmt.Printf("%#v",p)
}

结构体变量,可以通过 . 访问并且设置结构体字段的值

type person struct {
    name, city string
    age        int8
}
func main () {
    var p person // 声明结构体的类型的变量

    // 通过`.`访问并且设置结构体字段的值
    p.name = "张三"
    p.city = "厦门"
    p.age = 30
    fmt.Printf("p=%#v\n",p)
    fmt.Println(p.name)
}

点号不仅可以作用在结构体变量上,还可以作用在结构体指针上

type person struct {
  name, city string
   age        int8
}

func main() {
   var p *person = &person{}
   (*p).name = "张三"
  (*p).city = "厦门"
  (*p).age = 30
   fmt.Printf("p=%#v\n", (*p))
  fmt.Println((*p).name)
}

还可以获取成员变量的指针,通过指针来访问成员变量

type person struct {
 name, city string
   age        int8
}

func main() {
   var p person = person{}
 name := &p.name
 fmt.Println(*name)
}

匿名结构体

正常情况下都是先定义结构体,然后把变量声明为结构体的,但是匿名结构体主要用于临时使用,不需要先定义结构体的类型,可以直接把变量声明为没有名称的结构体

func main () {
    // 不需要type定义结构体
    var user struct{
        name string
        married bool
    }
    user.name = "张三"
    user.married = false
    fmt.Println(user)
}

指针类型的结构体

使用 new 关键字对结构体实例化,可以得到结构体的指针

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

还有一种方式也可以快速的获取结构体的指针,那就是 & 符号,person{} 表示对 person 结构体进行初始化,然后用 & 符号进行取值

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

然后可以同 * 号来访问指针地址的实际位置

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

正常情况下,要改变指针的实际值,都需要用 * 号来指向实际值,然后修改,但是每次都这样就显得麻烦,所以在结构体中,可以省略这步骤,直接访问,这个是语法糖的形式,如下所示也是可以的

var p = new(person)
p.name = "张三"
p.city = "厦门"
p.age = 30
fmt.Printf("%#v\n", p)

结构体内存布局

结构体变量占用一块连续的内存,也就是说字段在内存中是连续的,但是空的匿名结构体是不占用内存空间的

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

结构体构造函数

结构体没有构造函数,但是可以自己实现,其实就是构造一个结构体类型变量,并且赋值,并且函数名称约定成俗的以 new 开头。结构体是值类型,赋值的话是拷贝,所以复杂的结构体赋值会影响性能,因此构造函数最好返回指针类型

type person struct {
   name, city string
   age        int
}

func newPerson(name string, city string, age int) *person {
  return &person{
     name: name,
     city: city,
     age:  age,
  }
}
func main() {
 p := newPerson("小明", "厦门", 18)
  fmt.Printf("%#v\n", p) // &main.person{name:"小明", city:"厦门", age:18}
}

方法

Go 语言中,接收者的类型只能为用关键字 type 定义的类型,例如自定义类型,结构体。类型的方法指明该方法属于某个类型,只有这个类型的实例才能调用。同一个接收者的方法名不能重复。值作为接收者无法修改其值,如果有更改需求,需要使用指针类型。

type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
  fmt.Println("Hello, 我是一个int。")
}

同样,对结构体这种自定义类型,也可以添加自己的方法函数

type person struct {
    name, city string
   age        int
}

func (p person) dream() {
    fmt.Printf("%s的梦想是赚大钱", p.name)
}
func main() {
 p := person{
        name: "小明",
       city: "厦门",
       age:  18,
   }
   p.dream()
}

关于接受者变量和类型有几个要注意的是:

  1. 接受者变量的名称,一般使用类型的首个小写字母,例如上例中 MyInt 类型的接受变量就用 m
  2. 接受者变量的类型有值类型和指针类型,但是并不要求调用者也要对于的值类型或者指针类型,调用者只要是跟接受者同一个类型就好了,编译器会自动转换;值类型的接受者是对实例的拷贝,方法内部的改变不会影响到调用者实例本身;指针类型的接收者,方法内部对实例的改变,将会影响到实例本身的值。
  3. 结构体方法最后统一使用值接受者,或者指针接受者,一般统一使用指针接受者,保持统一,不然 idea 会警告

如果构造函数返回的是接口类型,并且接口实例的方法是指针接受者,那么构造必须返回指针,不然会报错

type IServer interface {
   Start()
}
type Server struct {
    Name      string
    IPVersion string
    IP        string
    Port      int
}

// 指针类型接受者
func (s *Server) Start() {
}

// 返回接口类型
func NewServer(name string) IServer {
   // !!因为接受者是指针类型,所以必须返回指针
    s := &Server{
       Name:      name,
        IPVersion: "tcp4",
        IP:        "0.0.0.0",
     Port:      8999,
    }
   return s
}

往值上添加方法,接受者就是值本身

type myInt int  

func (m myInt) PrintValue() {  
   fmt.Println(m)  
}  

func main() {  
   var m myInt  
   m = 10  
   m.PrintValue() // 10  
}

结构体的嵌套

一个结构体中可以包含另一个结构体或结构体指针,就好比 json 对象中,某个字段又是一个 json 对象

type Address struct {
    Province string
 City     string
}

type User struct {
  Name    string
  Gender  string
  Address Address // 嵌套另外一个结构体
}

func main() {
  userInfo := User{
       Name:   "小王子",
        Gender: "男",
      Address: Address{
           Province: "山东",
           City:     "威海",
       },
  }
   fmt.Printf("%#v\n", userInfo) // main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

但是需要注意的是,如果嵌入的结构体是本身,那么只能用指针。请看以下例子。

type Tree struct {
    value       int
 left, right *Tree
}

func main() {
 tree := Tree{
       value: 1,
       left: &Tree{
            value: 1,
           left:  nil,
         right: nil,
     },
      right: &Tree{
           value: 2,
           left:  nil,
         right: nil,
     },
  }

   fmt.Printf(">>> %#v\n", tree)
}

如果嵌套结构体的字段是匿名字段,那么访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找,这个特性很重要,我们就可以实现类似类里的属性和方法的覆盖特性。

type Address struct {
    Province string
 City     string
}

type User struct {
  Name   string
   Gender string
   Address
}

func main() {
   userInfo := User{
       Name:   "小王子",
        Gender: "男",
      Address: Address{
           Province: "山东",
           City:     "威海",
       },
  }
   fmt.Println(userInfo.City) // 输出了匿名字段Address结构体上的字段了
}

如果嵌套的结构体数量一多,很可能字段会重复,如果利用嵌套匿名字段结构体的查找规则,很可能会出问题,所以如果嵌套比较多结构体,最好指定具体的内嵌结构体字段名会比较清晰

//Address 地址结构体
type Address struct {
 Province   string
   City       string
   CreateTime string // 和email结构体中的createTime重复
}

//Email 邮箱结构体
type Email struct {
   Account    string
   CreateTime string
}

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

func main() {
 var user3 User
  user3.Name = "沙河娜扎"
   user3.Gender = "男"
    // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime,会报错
    user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
  user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
}

利用结构体的嵌套特性,我们就可以实现结构体的继承了。

//Animal 动物
type Animal struct {
  name string
}

func (a *Animal) move() {
   fmt.Printf("%s会动!\n", a.name)
}
func (a *Animal) eat() {
   fmt.Printf("%s会吃!\n", a.name)
}

//Dog 狗
type Dog struct {
   Feet   int8
 Animal //通过嵌套匿名结构体实现继承
}

// 重写父类的方法
func (d *Dog) eat() {
  fmt.Printf("%s会大口的吃~\n", d.name)
}

// 子类新定义的方法
func (d *Dog) wang() {
   fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    dogInfo := &Dog{
        Feet: 4,
        Animal: Animal{
         name: "旺财",
       },
  }
   dogInfo.eat()
   dogInfo.wang() //旺财会汪汪汪~
    dogInfo.move() //旺财会动!
}

结构体的比较

前提是结构体中的字段类型是可以比较的,应该是各个字段之间进行比较,如果每个字段的值都相等的话,就是相等

type Tree struct {
   value       int
 left, right *Tree
}

func main() {
 tree1 := Tree{
      value: 2,
   }

   tree2 := Tree{
      value: 1,
   }

   fmt.Printf(">>> %#v\n", tree1 == tree2)
}
0

评论 (0)

取消