Go 快速开发 | 08 - Go 中的结构体

286 阅读7分钟

一、结构体

Go 语言中的结构体就类似其他面向对象语言中的类,但又不完全一样。Go 语言中通过结构体的内嵌再配合接口比面向对象中的类具有更高的扩展性和灵活性。

Go 中的结构体是一种复合数据类型,是由基本类型组成的,Go 的结构体没有实例化之前就是对内存的一种描述方式,只有实例化后才能给结构体分配内存。

结构体的成员成为字段,类似对象中的属性,这些字段必须又有自己的类型和值,且必须字段名必须唯一,当然字段也可以是另一个结构体,也就是结构体的嵌套。

结构体的定义需要使用到 struct 关键字

type 结构体名 struct {
    字段名1 字段类型
    字段名2 字段类型
    字段名3 字段类型
    .............
}

结构体实例化

func main(){
   var cat Cat
   cat.Name = "猪猪"
   cat.Age = 1

   fmt.Println(cat, cat.Name, cat.Age)

}

type Cat struct {
   Name string
   Age int
}

执行上述代码,输出结果如下:

{猪猪 1} 猪猪 1

Go 中是严格区分大小写的,结构体中属性必须要使用大写字母开头,小写字母开头表示私有属性,无法解析

使用 new 关键字实例化结构体

实际应用中还可以通过 new 关键字来实例化结构体,返回结构体的内存地址,通过 \*结构体 可以获取到实例化的结构体的内容

func main(){

   var cat = new(Animal)
   cat.Name = "六六"
   cat.Age = 1

   fmt.Printf("%#v\n", cat)

}

type Animal struct {
   Name string
   Age int
}

执行上述代码,输出结果如下:

&main.Animal{Name:"六六", Age:1}

我们知道 new 返回的是一个内存地址,上述代码中 new 函数的返回值 cat (一个内存地址)直接进行结构体属性的赋值,这是因为在 Go 的底层 cat.Name 就等于 *cat.Name。

Go 中支持结构体指针直接访问结构体属性。

键值对实例化结构体

func main(){

   tony := Human {
      Name: "tony",
      Age: 18,
   }

   fmt.Printf("%#v", tony)
}

type Human struct {
   Name string
   Age int
}

执行上述代码,输出结果如下:

main.Human{Name:"tony", Age:18}

使用键值对方式初始化时,要注意在最后一个属性赋值后加上,,否则会报错。

二、结构体方法

Go 语言中没有其他面向对象语言中类的概念,但是可以给结构体定义方法,既定义了接收者的函数。

Go 语言中的方法是一种作用于特定类型变量的函数,这种特定类型变量叫做接收者,接收者的概念类似其他面向对象语言中的 this 或者 self。

func (接收者变量 接收者类型) 方法名(参数列表) (返回值类型) {
    // 函数体代码块
}

结构体方法和接收者

func main(){

   person := Person{
      Name: "zhangsna",
      Age: 18,
   }

   person.outputInfo()
}

type Person struct {
   Name string
   Age int
}

func (person Person) outputInfo() {
   fmt.Println(person.Name, person.Age)
}

执行上述代码,输出结果如下:

zhangsna 18

通过设置接收者,将结构体和方法进行绑定,只有通过指定的接收者结构体的实例化对象才能调用这个方法,这就类似于面向对象语言中在类中定义的实例化方法,只不过在 Go 语言中,结构体和结构体方法是分开写的,并且结构体方法中要指定结构体的类型。

值类型和指针类型接收者

结构体方法指定接收者时可以指定值类型,也可以指定指针类型

  • 值类型接收者
    • 当方法作用域值类型接收者时,Go 会在代码运行时将接收者的值复制一份
    • 在值类型接收者的方法中可以获取接收者的成员值,但是更新操作只针对复制的值,无法对接收者变量本身产生影响
  • 指针类型接收者
    • 指针类型的接收者有一个结构体的指针组成
    • 由于指针的特性,调用方法修改接收者指针的任意成员变量,在方法结束后,修改都是有效的
    • 类似其他面向对象语言中的 this 或者 self
func main(){

   person := Person{
      Name: "zhangsna",
      Age: 18,
   }

   person.updateInfoByCopyVal()
   fmt.Println(person)
   person.updateInfoByDirectValue()
   fmt.Println(person)
}

type Person struct {
   Name string
   Age int
}

// 值类型接收者函数
func (person Person) updateInfoByCopyVal() {
   person.Name = "ZS"
   person.Age = 81
   fmt.Println(person)
}


// 指针类型接收者函数
func (person *Person) updateInfoByDirectValue(){
   person.Name = "ZSAN"
   person.Age = 91
   fmt.Println(person)
}

执行上述代码,输出结果如下:

{ZS 81}
{zhangsna 18}
&{ZSAN 91}
{ZSAN 91}

当方法的接收者类型前面有 * 时就是指针类型接收者,指针类型接收者是直接使用的接收者本身,所有任何修改都会生效,而值类型接收者使用的是拷贝的值,任何修改只会在当前函数中生效,对原始接收者没有任何影响。

三、结构体的序列化和反序列化

序列化

Go 中的结构体类似于其他面向对象语言中的类,所以 Go 语言写的服务器端与客户端通信都是通过实例化结构体的序列化与反序列化来完成的。

序列化就是将实例化的结构体转成 JSON 格式的字符串,反序列化就是将 JSON 字符串转换成结构体对象。

Go 中结构体的序列化与反序列化需要使用到 encoding/json 这个 Package。

package main

import (
   // 导入 json 包
   "encoding/json"
   "fmt"
)

func main(){

   person := Person{
      Name: "zhangsna",
      Age: 18,
   }

   fmt.Printf("%#v\n", person)
   
   // 序列化
   var p, _ = json.Marshal(person)
   jsonP := string(p)
   fmt.Println(jsonP)
}

type Person struct {
   Name string
   Age int
}

执行上述代码,输出结果如下:

main.Person{Name:"zhangsna", Age:18}
{"Name":"zhangsna","Age":18}

通过 encoding/json 包下的 Marshal 方法可以实例化的结构体对象转换成 JSON 格式的字符串,但是 JSON 格式字符串中的 Key 都是大写的。

Go 是区分大小写的,Go 中大写字母定义的标识符是可以被外界应用的,小写的标识符是私有的,不可以被外界引用的。

如何将序列化后的 JSON 格式中 Key 的首字母变为小写?可以通过 Tag 标签来给结构体属性增加别名。

Tag 标签

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

Tag 标签定义在结构体字段的后面,通过一对反括号 `` 包裹起来,由一个或者多个键值对组成,多个键值对之间使用空格分隔,注意键值对的值要使用双引号包裹。

type 结构体 struct {
    结构体字段1 字段类型 `k1:"v1" k2:"v2"` // Tag 标签
}

注意不要在 Tag 标签的键值对中冒号后面增加空格,结构体标签解析代码的容错能力差,一旦格式写错,编译和运行时不会提示任何错误,通过反射也无法正确取值。

修改上述代码,只需修改结构体的定义即可

// 其余代码保持不变

type Person struct {
   Name string `json:"name"`
   Age int `json:"age"`
}

执行上述代码,输出结果如下:

main.Person{Name:"zhangsna", Age:18}
{"name":"zhangsna","age":18}

反序列化

反序列化既将 JSON 格式的字符串转换成结构体,这需要使用到 encoding/json 包下的 Unmarshal 函数,该函数的两个参数,一个字节数组和一个值,要将 JSON 字符串转换成字节数组。

func main(){

   jsonP := `{"name":"zhangsna","age":18}`

   var p Person

   isOk := json.Unmarshal([]byte(jsonP), &p)
   fmt.Println(isOk)
   fmt.Printf("%#v", p)

}

// Person 结构体定义不变,其余代码不变

执行上述代码,输出结果如下:

<nil>
main.Person{Name:"zhangsna", Age:18}

上述代码中传入 Umarshal() 函数的第二个值一定是一个指针类型既内存地址,如果直接传入结构体的初始化对象,则输出的结构体的字段是无法被赋值的。

Umarshal 函数的第二个参数直接传入 p,输出结果如下:

main.Person{Name:"", Age:0}

本文正在参加技术专题18期-聊聊Go语言框架