Golang复合类型

前言

上文 Golang 基本类型 中我们介绍了golang 基本类型的常见用法,本文将介绍 golang 中的复合数据类型,常用的复合数据类型有 array 数组,slice 切片,map 字典 和 struct 四种。

数组

数组是一个由固定长度特定类型元素组成的序列,由于长度固定,在实际业务场景下使用较为不便,因此在 go 中很少直接使用。

数组的长度是数组类型的一个组成部分,因此 [3]int 和 [4]int 是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

和大多数语言一样,go 的数组下表也是从 [0] 开始,到 [len - 1] 结束

var a [3]int
var q [3]int = [3]int{1,2,3}   // 1,2,3
var r [3]int = [3]int{1,2}     // 1,2,0

a[0] = 1
a[1] = 2
a[2] = 3

也可以使用 “…” 来声明数组,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算,因此,上面 q 数组的定义可以简化为

q := [...]int{1,2,3}
fmt.Println(reflect.TypeOf(q))  // 输出 [3]int

对于较大的数组,可以指定一个索引和对应值列表的方式初始化,如下,定义了一个含有100个元素的数组r,索引为 13 的位置值为 21,最后一个元素被初始化为 -1,其它元素都是用 0 初始化。

r := [...]int{13: 21, 99: -1}

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型

go 的 slice 底层是由数组实现的,一个slice由三个部分构成:指针、长度和容量。创建 slice 时,指针指向第一个slice元素对应的底层数组元素的地址,长度对应slice中元素的数目,容量表示创建数组时分配空间的初始大小,长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。

和数组不同的是,slice 之间不能使用 == 互相比较,slice 唯一合法的比较操作是和nil比较, 但是通常更推荐使用 len(s) == 0来判断,而不是 s == nil,否则对 []int{} 这种形式的 slice 判断会出现问题,可以参考如下代码。

var s []int 	// len(s) == 0, s == nil
s = []int(nil)  // len(s) == 0, s == nil
s = []int{}     // len(s) == 0, s != nil

slice 基本操作

初始化

使用 make([]Type, len, cap) 初始化 slice, cap 参数可以缺省, 缺省时默认 cap 等于 len

sl_null := []int{} 	// 创建空slice
sl0 := make([]int, 0, 3)  // 创建 cap 为 3 的空 slice []
sl1 := make([]int, 0)  // 创建 []
sl2 := make([]int, 3)  // 创建 [0,0,0]

// 创建有初始内容的 slice
sl4 := []int{1,2,3}    // 类型为 []int
// 注意区分 slice 和 数组的初始化
// 数组的初始化为 s := [...]{1,2,3}, 类型为 [3]int

在 slice 末尾添加元素

添加元素时,如果添加完成后 len 大于当前 cap, slice 会进行扩容, 每次扩容 cap 的大小会翻一倍

sl3 = append(sl3, 4)   // 添加单个元素
sl3 = append(sl3, 5, 6, 7)  // 添加多个元素

sl4 := []int{8, 9, 10}
sl3 = append(sl3, sl4...)   // 将 sl4 展开,把所有元素添加到 sl3 末尾
// 修改 sl3 不会改变 sl4 的元素

获取 slice 长度和容量

内置的 lencap 函数分别返回slice的长度和容量。

len_sl3 = len(sl3)   // int
cap_sl3 = cap(sl3)   // int

查看某元素是否在 slice 里

没有内置函数,需要自己遍历实现, 对 slice 遍历时取到两个值,第一个为 slice 的下标,第二个为下标对应的值。

for _, num := rang num_slice {
	if num == target {
		// ...
	}
}

删除元素

go 没有直接删除元素的库函数,通过切片赋值来实现

(插入操作也是类似)

// 从起始位置删除
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

// 从中间位置删除,其实是将两个 slice 片段合并,注意后半段需要使用 ... 展开
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素

// 从尾部删除
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

插入

a := []int{1,2,4,5,6,7,8}
temp := append([]int{3}, a[2:]...)
a = append(a[:2], temp...)
// output a: [1 2 3 4 5 6 7 8]

//下面的写法等价:
a = append(a[:2], append([]int{3},a[2:]...)...)

切片

介绍切片和复制之前,需要先简单介绍一下浅拷贝和深拷贝。

浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响

深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝, 拷贝前后的两个对象互不影响

切片操作为浅拷贝,sli[m:n]会取得从 sli[m]sli[n - 1] 之间的元素,但是不会复制切片的数据。 它创建一个指向原始数组的新切片值。

因此,修改重新切片的元素会修改原始切片的元素。实际使用中如果忽视了这一点,就很容易发生预期之外的错误。

注意: 与 python 不同, golang 的切片索引不能为负数

sli := []int{1,2,3,4,5,6}	// [1 2 3 4 5 6]
sli2 := sli[2:5]	// [3 4 5]
sli2[0] = 10	// [10 4 5]
fmt.Println(sli) // [1 2 10 4 5 6]

复制

复制操作为深拷贝,copy 后, 修改 t 的内容不会改变 s 的内容

如果 t 的 len 小于 s, 则将 s 的内容截断后复制

s := []int{1,2,3,4,5,6,7,8,9,10}
t := make([]int, len(s), cap(s))
copy(t,s)   // 将 s 复制给 t [1 2 3 4 5 6 7 8 9 10]
t[0] = 10 // [10 2 3 4 5 6 7 8 9 10]
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10]

反转数组

s := []int{1,2,3,4,5,6,7,8,9,10}
for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 {
  s[i], s[j] = s[j], s[i]
}

map

golang 的 map 为哈希表,是一个无序的 key/value 对的集合,其中所有的 key 都是不同的,通过给定的 key 可以在常数时间复杂度内检索、更新或删除对应的 value。

map 的 key 必须是可比较的数据类型,value 的类型没有要求(如 slice 不能做 key,只能做 value)

map 基本操作

初始化

// 声明变量,默认 map 是 nil
var map_variable map[key_data_type]value_data_type   // 声明 类型为 map[K]V
map_variable = make(map[key_data_type]value_data_type) // 定义

// 使用 make 函数
map_variable := make(map[key_data_type]value_data_type) // 如 dic := map[string]int

// 等价于
map_variable := map[key_data_type]value_data_type{}
// 这种方式可以构建带初始值的map,如:
age:= map[string]int {
  "Tony" : 35,
  "Wallace" : 24,    // 注意最后也要带逗号
}

判断 key 是否存在

// 判断某个 key n 是否在字典中
// 若存在,ok 为 True,否则为 False
if _, ok := map_name[n]; ok {
    // ...
}

添加或者修改元素

若 key 存在则为修改,否则为添加

map_name[key] = value  // 如 age["Tony"] = 35

删除元素

Go 的删除操作和访问操作都是安全的,即使访问或者删除的 key 不存在,也不会报错,访问不存在的 key 返回 0

delete(age, "Tony")

注意:map 可以对 value 进行各种数值操作,但是 value 并不是一个变量,因此不能对 map 的元素进行取地址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

n = &age["bob"]  // Compile Error: cannot take address of map element

map 迭代

Map的迭代顺序是随机的,并且不同的哈希函数实现可能导致不同的遍历顺序

for key, value := range age {
  fmt.Println(key, value)
}

golang 没有内置的集合功能,可以使用 map 来实现集合,因为 key 是唯一的(如使用 key 代表集合元素,value 全部置为 1)

Struct

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。

定义和声明

type employee struct {     // 定义 Employee 类型的结构体
  id 			int
  name 		string
  address string
  title 	string
}

var wallace employee		// 声明一个 Employee 类型的变量 wallace
wallace.id = 12345    // 使用点操作符访问结构体变量

tony := Employee {3333, "Tony", "Beijing", "Engineer"}  // 初始化 Employee 变量
cassie := Employee{
  id:				3333
  name:			"cassie"
  address:	"shenzhen"
  title:		"engineer"
}

注意:

  • 访问限制通过首字母大小写决定
  • 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用 == 或 ! = 运算符进行比较。将比较两个结构体的每个成员。

结构体指针

通过再方法内部需要改变结构体成员变量值,则必须要使用结构体指针进行传值,因为在 go 中,所有的函数参数都是值拷贝传入的,函数参数并不是函数调用时的原始变量。

func AwardAnnualRaise(e *Employee) {
	e.Salary = e.Salary * 105 / 100
}

此外,一个命名为S的结构体类型将不能再包含S类型的成员,但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。

type tree struct {
	value  int
	left   *tree
	right  *tree
}

而且用指针作为参数效率会更高,因此通常函数的入参和回参中的结构体都会使用指针的方式。

打印结构体值

推荐使用 %+v 而不是 %v,输出会包括结构体的成员名称和值

package main

import "fmt"

type info struct {
    name string
    id int
}

func main()  {
    v := info{"Nan", 33}
    fmt.Printf("%v\n", v)
    fmt.Printf("%+v\n", v)
    fmt.Printf("%#v\n", v)
    fmt.Printf("%T\n", v)
}

运行结果如下:

{Nan 33}
{name:Nan id:33}
main.info{name:Nan id:33}
main.info

匿名成员

为了便于维护,通常会将有相同功能的属性独立出来,如:

type Point struct {
    X, Y int
}

type Circle struct {
    Center Point
    Radius int
}

type Wheel struct {
    Circle Circle
    Spokes int
}

但是这种修改会导致访问成员变量变得非常繁琐:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

可以使用匿名成员解决这个问题,只声明成员的数据类型而不指定成员的名字,匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

type Point struct {
  X, Y int
}

type Circle struct {
  Point
  Radius int
}

type Wheel struct {
  Circle
  Spokes int
}

// 得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

// 不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

// 正确的初始化方式为下列两种形式,两种形式是等价的
w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

其他复合类型

Json

由于简洁性、可读性和流行程度等原因,JSON是结构化信息的标准协议中应用最广泛的一个。JSON 使用键值对的方式表示结构化信息,并用逗号分隔,要注意的是在最后一个成员或元素后面并没有逗号分隔符,形如:

{
    "id" : 1,
    "name": "wallace",
	"teams":{
		"team_id":1,
		"team_name":"wallace team"
	},
	"skills":["go","mysql","redis"],
    "description": "test"
}

Marshaling

将一个 Go 语言中的结构体 slice 转为 JSON 的过程叫编码(marshaling),编组通过调用json.Marshal函数完成,转换为 json 的 key 值为结构体中 json tag 指定的内容。

如以下结构体:

type Team struct {
	TeamID   int    `json:"team_id" db:"team_id"`
	TeamName string `json:"team_name" db:"team_name"`
}

type Member struct {
	ID          int      `json:"id" db:"id"`
	Name        string   `json:"name" db:"name"`
	Teams       *Team    `json:"teams" db:"teams"`
	Skills      []string `json:"skills" db:"skills"`
	Description string   `json:"description" db:"description"`
}

对结构体对象进行 marshaling,编码为 json 字符串

wallace := &Member{
	ID:          1,
	Name:        "wallace",
	Teams:       &Team{
		TeamID:   1,
		TeamName: "team 1",
	},
	Skills:      []string{"go", "mysql", "redis"},
	Description: "test",
}

js, err := json.Marshal(wallace)
if err != nil {
	log.Fatalf("JSON marshaling failed: %s", err)
}

fmt.Printf("%s\n", js)

输出为:

{"id":1,"name":"wallace","teams":{"team_id":1,"team_name":"team 1"},"skills":["go","mysql","redis"],"description":"test"}

也可以使用 json.MarshalIndent 函数产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

data, err := json.MarshalIndent(wallace, "", " ")
if err != nil {
	log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

输出为:

{
    "id": 1,
    "name": "wallace",
    "teams": {
        "team_id": 1,
        "team_name": "team 1"
    },
    "skills": [
        "go",
        "mysql",
        "redis"
    ],
    "description": "test"
}

在编码时,只有可导出的结构体成员才会被编码。

Unmarshal

编码的逆操作是解码(unmarshaling),将 json 字符串转换为结构体。

member := &Member{}
err = json.Unmarshal(data, member)
if err != nil {
	log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Printf("%+v", *member)

输出为:

{ID:1, Name:"wallace", Teams:(*timetest.Team)(0xc42000a3e0), Skills:[]string{"go", "mysql", "redis"}, Description:"test"}

有一点需要注意,对于结构体中没有赋值的数组,json.Unmarshal 会解析为 null,有可能会导致前端框架的报错,所以建议把结构体中的空数组都初始化为 []

评论 1 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

Wallace JW

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值