LOADING

正在加载

Go基础学习(陆)——结构体

壹 介绍

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型(这是数组与结构体区别),这种数据类型叫结构体,英文名称struct

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。Go语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。结构体表示一项记录,比如保存图书馆的书籍记录,每本书有以下属性:

  • Title :标题
  • Author : 作者
  • Subject:学科
  • ID:书籍ID

这些都可以用结构体包装起来。在Go语言中没有类的概念,也不支持类的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

贰 类型别名和自定义类型

2.1 自定义类型

在Go语言中有一些基本的数据类型,如string整型浮点型布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

2.2 类型别名

类型别名是Go1.9版本添加的新功能。类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

type TypeAlias = Type

我们之前见过的runebyte就是类型别名,他们的定义如下:

type byte = uint8
type rune = int32

2.3 类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt		可以看到是一个全新的类型
    fmt.Printf("type of b:%T\n", b) //type of b:int		只是给int取了别名
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

叁 定义声明结构体

结构体定义需要使用typestruct语句。

  • struct语句定义一个新的数据类型,这个数据类型就是一个结构体,结构体中有一个或多个成员,这些成员有不同的数据类型组成。

结构体的格式如下:

type 类型名 struct{
    //可以看出不需要加var
    字段名 字段类型
    字段名 字段类型
    ...
}
// 类型名:标识自定义结构体的名称,在同一个包内不能重复,就类似与int、string这些类型名。
// 字段名:表示结构体内的属性名。结构体中的字段名必须唯一,结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
// 字段类型:表示结构体内属性具体的类型。

例子:

package main
import "fmt"
type Books struct {
    title, author   string //标题、作者,可以看到该定义可以像定义变量语言多个定义
    subject string //学科
    book_id int    //书籍ID
}
// 上面结构体的类型名:Books,有title、 author、subject、book_id这些属性
func main() {
    //变量创建一个结构体
    Book1 := Books{"假如给我三天光明", "海伦凯勒", "文学", 1}
    fmt.Println(Book1)
    //直接输出创建一个结构体
    fmt.Println(Books{"假如给我三天光明", "海伦凯勒", "文学", 1})
    //使用 key => value 格式
    fmt.Println(Books{title: "假如给我三天光明", author: "海伦凯勒", subject: "文学", book_id: 1})
    //忽略的字段为 0 或 空
    fmt.Println(Books{title: "假如给我三天光明"})
}

08e66b6cc1fb0b93f62d64b619a8e897.png

肆 结构体实例化与访问结构体成员

4.1 实例化与访问结构体成员

  • 结构体实例化
    同样结构体与其他数据类型一样需要实例化,才能分配内存空间,也就是必须实例化后才能使用结构体的字段。由于结构体本身也是一种类型,我们可以像之前声明内置类型一样使用var关键字声明结构体类型。

    var 结构体实例 结构体类型
    
  • 访问结构体成员
    如果要访问结构体成员,需要使用点号.操作符,格式为:

    结构体.成员名
    

4.2 基本实例化

package main

import "fmt"

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

4.3 匿名结构体

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

package main
import (
    "fmt"
)
func main() {
    user := struct {
        Name string
        Age  int
    }{
        "A7cc",
        18,
    }
    fmt.Printf("%#v\n", user) //struct { Name string; Age int }{Name:"A7cc", Age:18}
}

小知识:
有些项目会有这种情况:struct {}、struct {} {}
struct {}是一个无元素的结构体类型,通常在没有信息存储时使用。优点是大小为0,不需要内存来存储struct {}类型的值,注意是没有没有内存存储
struct {} {}是一个复合字面量,它构造了一个struct {}类型的值,该值也是空
这种形式的结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。一是节省资源,二是空结构体本身就具备很强的语义,即这里不需要任何值,仅作为占位符。

var set map[string]struct{} 
set = make(map[string]struct{})

set["red"] = struct{}{} // struct{}{}  构造了一个struct {}类型的值
set["blue"] = struct{}{}

_, ok := set["red"]
fmt.Println("Is red in the map?", ok)
_, ok = set["green"]
fmt.Println("Is green in the map?", ok)
// Is red in the map? true
// Is green in the map? false

map可以通过comma ok机制来获取该key是否存在_, ok := map["key"],如果没有对应的值,okfalse,这样可以通过定义成map[string]struct{}的形式,值不再占用内存。其值仅有两种状态:有或无。

  • 其他知识点
    chan struct{}:可以用作通道的退出
    两个structt{}{}地址相等

4.4 创建指针类型结构体

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

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

从打印的结果中我们可以看出p2是一个结构体指针。需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员

var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}

4.5 取结构体的地址实例化

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

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}

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

伍 结构体初始化

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

type person struct {
    name string
    city string
    age  int8
}
// 这个结构体会在下面小节延用

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

5.1 使用键值对初始化

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

p5 := person{
    name: "A7cc",
    city: "中国",
    age:  18,
    // 初始化最后一定要加英文的 , 逗号
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"A7cc", city:"中国", age:18}

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

p6 := &person{
    name: "A7cc",
    city: "中国",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"A7cc", city:"中国", age:18}

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

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

5.2 使用值的列表初始化

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

// 也可以对结构体指针这样定义
p8 := person{
    "A7cc",
    "中国",
    18,
}
fmt.Printf("p8=%#v\n", p8) //p8=main.person{name:"A7cc", city:"中国", age:18}

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

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

5.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)	// n.a 0xc0000a0060
fmt.Printf("n.b %p\n", &n.b)	// n.b 0xc0000a0061
fmt.Printf("n.c %p\n", &n.c)	// n.c 0xc0000a0062
fmt.Printf("n.d %p\n", &n.d)	// n.d 0xc0000a0063

【进阶知识点】关于Go语言中的内存对齐推荐阅读:在 Go 中恰到好处的内存对齐

5.4 空结构体

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

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

陆 结构体标签(Tag)

6.1 struct成员变量标签(Tag)说明

golang中,命名都是推荐都是用驼峰方式,并且在首字母大小写有特殊的语法含义:包外无法引用。但是由经常需要和其它的系统进行数据交互,例如转成JSON格式,存储到mongodb等等。这个时候如果用属性名来作为键值可能不一定会符合项目要求。所以就多了反引号的内容,在golang中叫标签(Tag),在转换成其它数据格式的时候,会使用其中特定的字段作为键值。

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    // Tag标签说明,我们只需要在元素后面加上`json:"自定义属性名"`
    UserId   int    `json:"user_id" bson:"user_id"`
    UserName string `json:"user_name" bson:"user_name"`
}

func main() {
    u := &User{UserId: 1, UserName: "tony"}
    j, _ := json.Marshal(u)
    fmt.Println(string(j))

    // 输出内容:
    // {"user_id":1,"user_name":"tony"}
    // 如果在属性中不增加标签说明,则输出:
    // {"UserId":1,"UserName":"tony"}
    // 可以看到直接用struct的属性名做键值。
    // ==其中还有一个bson的声明,这个是用在将数据存储到mongodb使用的==
}

6.2 struct成员变量标签(Tag)获取

那么当我们需要自己封装一些操作,需要用到Tag中的内容时,咋样去获取呢?这边可以使用反射包reflect中的方法来获取:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

func main() {
    type User struct {
        UserId   int    `json:"user_id" bson:"user_id"`
        UserName string `json:"user_name" bson:"user_name"`
    }
    // 输出json格式
    u := &User{UserId: 1, UserName: "tony"}
    j, _ := json.Marshal(u)
    fmt.Println(string(j))
    // 输出内容:{"user_id":1,"user_name":"tony"}

    // 获取tag中的内容
    t := reflect.TypeOf(u)
    field := t.Elem().Field(0)
    fmt.Println(field.Tag.Get("json"))
    // 输出:user_id
    fmt.Println(field.Tag.Get("bson"))
    // 输出:user_id
}

6.3 自定义tag

我们还可以自定义Tag。

// 获取tag中的内容
typeof := reflect.TypeOf(u)
field := typeof.Elem().Field(0)
fmt.Println(field.Tag.Get("json"))
// 输出:user_id
fmt.Println(field.Tag.Get("bson"))
// 输出:user_id
fmt.Println(field.Tag.Get("test"))
// 输出:test

6.4 omitempty——可以忽略不必要的信息

6.4.1 使用介绍

Golang 的结构体定义中添加 omitempty 关键字,来表示一条信息如果没有提供,在序列化成 JSON 的时候就不要包含其默认值。

例如:

package main

import (
    "encoding/json"
    "fmt"
)

type address struct {
    Street  string `json:"street"`  // 街道
    Ste     string `json:"suite"`   // 单元(可以不存在)
    City    string `json:"city"`    // 城市
    State   string `json:"state"`   // 州/省
    Zipcode string `json:"zipcode"` // 邮编
}

func main() {
    data := `{
    "street": "200 Larkin St",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}`
    addr := new(address)
    json.Unmarshal([]byte(data), &addr)

    // 处理了一番 addr 变量...

    addressBytes, _ := json.MarshalIndent(addr, "", "    ")
    fmt.Printf("%s\n", string(addressBytes))
}

输出的是:

{
    "street": "200 Larkin St",
    "suite": "",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}

多了一行 "suite": "",而这则信息在原本的 JSON 数据中是没有的,我们更希望的是,在一个地址有 suite 号码的时候输出,不存在 suite 的时候就不输出,这时候我们就需要使用omitempty关键字。修改例子:

package main

import (
    "encoding/json"
    "fmt"
)

type address struct {
    Street  string `json:"street"`  // 街道
    // 在json内追加omitempty即可
    Ste     string `json:"suite,omitempty"`   // 单元
    City    string `json:"city"`    // 城市
    State   string `json:"state"`   // 州/省
    Zipcode string `json:"zipcode"` // 邮编
}

func main() {
    data := `{
    "street": "200 Larkin St",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}`
    addr := new(address)
    json.Unmarshal([]byte(data), &addr)

    // 处理了一番 addr 变量...

    addressBytes, _ := json.MarshalIndent(addr, "", "    ")
    fmt.Printf("%s\n", string(addressBytes))
}
// {
//     "street": "200 Larkin St",
//     "city": "San Francisco",
//     "state": "CA",
//     "zipcode": "94102"
// }

6.4.2 陷阱

带来方便的同时,使用 omitempty 也有些小陷阱。

6.4.2.1 关键字无法忽略掉嵌套结构体

还是拿地址类型说事,这回我们想要往地址结构体中加一个新 Coordinate 来表示经纬度,如果没有缺乏相关的数据,暂时可以忽略。

type Address struct {
    City       string     `json:"city"`
    Street     string     `json:"street"`
    ZipCode    string     `json:"zip_code,omitempty"`
    Coordinate coordinate `json:"coordinate,omitempty"`
}

type coordinate struct {
    Lat float64 `json:"latitude"`
    Lng float64 `json:"longitude"`
}

func TestMarshal(t *testing.T) {
    data := `{
        "city": "Beijing",
        "street": "a"
    }`
    addr := &Address{}
    json.Unmarshal([]byte(data), addr)

    addressBytes, _ := json.MarshalIndent(addr, "", "    ")
    fmt.Printf("%s\n", string(addressBytes))
}

// 输出是:
// {
//     "city": "Beijing",
//     "street": "a",
//     "coordinate": {
//         "latitude": 0,
//         "longitude": 0
//     }
// }

读入原来的地址数据,处理后序列化输出,我们就会发现即使加上了 omitempty 关键字,输出的 JSON 还是带上了一个空的坐标信息。为了达到我们想要的效果,可以把坐标定义为指针类型,这样 Golang 就能知道一个指针的空值是多少了,否则面对一个我们自定义的结构, Golang 是猜不出我们想要的空值的。于是有了如下的结构体定义:

type address struct {
    Street     string      `json:"street"`
    Ste        string      `json:"suite,omitempty"`
    City       string      `json:"city"`
    State      string      `json:"state"`
    Zipcode    string      `json:"zipcode"`
    Coordinate *coordinate `json:"coordinate,omitempty"`
}

type coordinate struct {
    Lat float64 `json:"latitude"`
    Lng float64 `json:"longitude"`
}
// 输出是:
// {
//     "street": "200 Larkin St",
//     "city": "San Francisco",
//     "state": "CA",
//     "zipcode": "94102"
// }

6.4.2.2 想要传入零值

对于用 omitempty 定义的 字段 ,如果给它赋的值恰好等于默认空值的话,在转为 json 之后也不会输出这个 字段 。比如说上面定义的经纬度坐标结构体,如果我们将经纬度两个 字段 都加上 omitempty

type coordinate struct {
    Lat float64 `json:"latitude,omitempty"`
    Lng float64 `json:"longitude,omitempty"`
}

func TestMarshal(t *testing.T) {
    data := `{
        "latitude": 1.0,
        "longitude": 0.0
    }`
    c := &coordinate{}
    json.Unmarshal([]byte(data), c)
    fmt.Printf("%#v\n", c)

    addressBytes, _ := json.MarshalIndent(c, "", "    ")
    fmt.Printf("%s\n", string(addressBytes))
}

// &omitempty.coordinate{Lat:1, Lng:0}
// {
//     "latitude": 1
// }

这个坐标的longitude消失不见了!但我们的设想是,如果一个地点没有经纬度信息,则悬空,这没有问题,但对于原点坐标,我们在确切知道它的经纬度的情况下,0.0仍然被忽略了。正确的写法也是将结构体内的定义改为指针:

type coordinate struct {
    Lat *float64 `json:"latitude,omitempty"`
    Lng *float64 `json:"longitude,omitempty"`
}

// &omitempty.coordinate{Lat:(*float64)(0xc0000a6288), Lng:(*float64)(0xc0000a6298)}
// {
//     "latitude": 1,
//     "longitude": 0
// }

这样空值就从 float640.0 变为了指针类型的 nil ,我们就能看到正确的经纬度输出。

6.5 inline——内嵌的结构体类型

有的时候我们需要结构体内嵌结构体,就是说我们并不想套娃地去表示内嵌的结构体,而是直接替换,这时候就可以使用inline进行说明:

package main

import (
    "encoding/json"
    "fmt"
)

type Project struct {
    Key   string `json:"key"`
    Value string `json:"value"`
}

type JiraHttpReqField struct {
    Project     `json:"project"`   // `json:",inline"`
    Summary     string `json:"summary"`
    Description string `json:"description"`
}

func main() {
    dataProject := Project{
        Key:   "name",
        Value: "zhangsan",
    }
    dataJiraHttpReqField := &JiraHttpReqField{
        Project:     dataProject,
        Summary:     "my summary",
        Description: "my description",
    }
    data, _ := json.Marshal(dataJiraHttpReqField)
    fmt.Println(string(data))
}

// 不使用inline,输出是:
// {"project":{"key":"name","value":"zhangsan"},"summary":"my summary","description":"my description"}

// 使用inline,输出是:
// {"key":"name","value":"zhangsan","summary":"my summary","description":"my description"}

可以发现使用inline会将内容值直接替换结构体内的变量名,而不是通过变量名来进行引入。

6.6 type——处理反序列化前后不一致的类型

有些时候,我们在序列化或者反序列化的时候,可能结构体类型和需要的类型不一致,这个时候可以指定其类型,目前支持stringnumberboolean

package main

import (
    "encoding/json"
    "fmt"
)

// Product _
type Product struct {
    Name      string  `json:"name"`
    ProductID int64   `json:"product_id,string"`
    Number    int     `json:"number,string"`
    Price     float64 `json:"price,string"`
    IsOnSale  bool    `json:"is_on_sale,string"`
}

func main() {

    var data = `{"name":"Xiao mi 6","product_id":"10","number":"10000","price":"2499","is_on_sale":"true"}`
    p := &Product{}
    err := json.Unmarshal([]byte(data), p)
    fmt.Println(err)
    fmt.Println(*p)
}
// 结果
<nil>
{Xiao mi 6 10 10000 2499 true}

6.7 短横线——不显示指定的JSON的数据

前面我们知道在 Golang 的结构体定义中 omitempty 关键字,是用来表示一条信息如果没有提供,在序列化成 JSON 的时候就不要包含其默认值,但是如果提供,就会显示,这时候就出现一个问题,如果我们需要隐藏该条信息,无论他是否存在值,都隐藏,那么我们可以使用短横线-

package main

import (
    "encoding/json"
    "fmt"
)

type address struct {
    Street  string `json:"street"`          // 街道
    Ste     string `json:"suite,omitempty"` // 单元,该信息如果有默认值就会输出
    Demo    string `json:"-"`               // 该信息不显示
    City    string `json:"city"`            // 城市
    State   string `json:"state"`           // 州/省
    Zipcode string `json:"zipcode"`         // 邮编
}

func main() {
    data := `{
    "street": "200 Larkin St",
    "Demo": "demo",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}`
    data1 := `{
    "street": "200 Larkin St",
    "Demo": "demo",
    "suite": "一单元",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}`
    addr := new(address)
    // 处理了一番 addr 变量...
    // 处理date
    json.Unmarshal([]byte(data), &addr)
    addressBytes, _ := json.MarshalIndent(addr, "", "    ")
    fmt.Printf("%s\n", string(addressBytes))
    // 处理date1
    json.Unmarshal([]byte(data1), &addr)
    addressBytes, _ = json.MarshalIndent(addr, "", "    ")
    fmt.Printf("%s\n", string(addressBytes))
}

// {
//     "street": "200 Larkin St",
//     "city": "San Francisco",
//     "state": "CA",
//     "zipcode": "94102"
// }
// {
//     "street": "200 Larkin St",
//     "suite": "一单元",
//     "city": "San Francisco",
//     "state": "CA",
//     "zipcode": "94102"
// }

可以看到用omitempty 关键字是只有在有数据的时候才会显示,而用短横线-是不会显示的,那什么时候不显示数据呢?例如该结构体存在密码信息,但是我不需要显示出来的时候,就可以使用短横线-表示。

avatar
小C&天天

修学储能 先博后渊


今日诗句