golang复合类型(代码片段)

WallaceJW WallaceJW     2022-12-13     586

关键词:

前言

上文 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]int1,2,3   // 1,2,3
var r [3]int = [3]int1,2     // 1,2,0

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

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

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

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

r := [...]int13: 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 := []int1,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 := []int8, 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 := []int1,2,4,5,6,7,8
temp := append([]int3, a[2:]...)
a = append(a[:2], temp...)
// output a: [1 2 3 4 5 6 7 8]

//下面的写法等价:
a = append(a[:2], append([]int3,a[2:]...)...)

切片

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

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

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

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

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

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

sli := []int1,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 := []int1,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 := []int1,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.infoname: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 = Wheel8, 8, 5, 20                       // compile error: unknown fields
w = WheelX: 8, Y: 8, Radius: 5, Spokes: 20 // compile error: unknown fields

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

w = Wheel
    Circle: Circle
        Point:  PointX: 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,有可能会导致前端框架的报错,所以建议把结构体中的空数组都初始化为 []

golang学习笔记-基础数据类型(代码片段)

今天开始更新go语言学习的内容,一方面可以记录自己的学习日常,还可以督促自己的学习。本博客中大部分内容,均是看来《go圣经》之后的总结。go语言中有四种数据类型:基础类型,复合类型,引用类... 查看详情

golang学习笔记-基础数据类型(代码片段)

今天开始更新go语言学习的内容,一方面可以记录自己的学习日常,还可以督促自己的学习。本博客中大部分内容,均是看来《go圣经》之后的总结。go语言中有四种数据类型:基础类型,复合类型,引用类... 查看详情

golang复合数据结构(代码片段)

查看详情

golang基本概念(代码片段)

类型18个基本类型:bool、string、rune、byte、int、uint、int8、uint、int8、int16、uint16、int32、uint32、int64、uint64、float64、complex64、complex1287个复合类型:array、struct、function、interface、slice、map、channel其中,slice、map和channel都 查看详情

golang学习随便记3(代码片段)

...合数据类型(构造类型)数组和C语言一样,golang数组是固定长度的,索引方式也一样,不同的是,golang数组元素默认就是初始化的(为该类型的0值)。遍历方式略有不同。golang数组也可以和C一样... 查看详情

golang学习随便记4(代码片段)

复合数据类型(构造类型)mapgolangmap是Hash表的引用,差不多就是PHP关联数组或者Python字典,当然C++STL也有map,但golangmap应该是unordered_map。map所有键必须同类型,值也必须同类型,这一点并不像PHP... 查看详情

golang学习随便记5(代码片段)

复合数据类型(构造类型)JSONgolang天生支持JSON和HTML,意味着它天生为网络编程服务。JSON使用的基本类型是数字(十进制或科学计数法)、布尔值(true或false)、字符串(Unicode码点序列,用\\... 查看详情

万字golang基础知识(肝爆三天三夜,手撕golang基本语法结构)(代码片段)

Golang基础知识1初识Golang1.1Go的语法要求1.1.1token1.2变量和常量变量常量1.3基本数据类型1.3.1布尔类型1.3.2整型1.3.3浮点型1.3.4复数类型1.3.5字符串1.3.6rune类型1.4复合数据类型1.4.1指针1.4.2数组1.4.3切片1.4.4map1.4.5struct1.5控制语句1.5.1if语... 查看详情

golang学习笔记-复合类型

在之前学习了go语言的基本数据类型,现在讨论go的复合数据类型,包括:数组,切片(slice),字典(map)和结构体。数组和结构体是聚合类型,它们的值由许多元素或成员字段的值组成。... 查看详情

golang学习笔记-复合类型

在之前学习了go语言的基本数据类型,现在讨论go的复合数据类型,包括:数组,切片(slice),字典(map)和结构体。数组和结构体是聚合类型,它们的值由许多元素或成员字段的值组成。... 查看详情

golang中struct和struct区别(代码片段)

struct是Go中的关键字,用于定义结构类型。例如:typeUserstructNamestringAgeintstruct:表示struct类型struct是一个无元素的结构体类型,通常在没有信息存储时使用。优点是大小为0,不需要内存来存储struct类型的值。struct:表示struct类型... 查看详情

复合(或引用)数据类型图解:及例题代码(代码片段)

复合(或引用)数据类型图解:及例题代码;varnum1=10;varnum2=num1;num2=20;alert(num1);//10alert(num2);//20 复合数据类型/引用数据类型数组、函数在我们复合数据类型里面,存储的都是数据的地址(房间号)。引用:变量里面存储的数据... 查看详情

golang类型转换(代码片段)

查看详情

golang类型断言(代码片段)

查看详情

golanginterface接口详解(代码片段)

前言在之前的文章中我们说过,golang是通过结构体(struct)-方法(method)-接口(interface)的组合使用来实现面向对象的思想。在前文Golang复合类型和Golangmethod方法详解已经详细介绍过struct和method,本文将介绍golang面向对象的另一... 查看详情

elasticsearch实战-复合数据类型(代码片段)

...文档数组,因此,每个JSON文档都内在地具有层次结构。复合数据类型是指数组类型,对象类型和嵌套类型,各个类型的特点分别是:数组字段是 查看详情

elasticsearch实战-复合数据类型(代码片段)

...文档数组,因此,每个JSON文档都内在地具有层次结构。复合数据类型是指数组类型,对象类型和嵌套类型,各个类型的特点分别是:数组字段是 查看详情

mongodb——索引类型之复合索引(compoundindex)(代码片段)

目录一、MongoDB官网地址二、复合索引(CompoundIndex)2.1、复合索引(CompoundIndex)的概述2.2、复合索引(CompoundIndex)的图解2.3、复合索引(CompoundIndex)的注意事项三、复合索引的创建一、MongoDB官网... 查看详情