最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

全球速读:Go 数据结构

来源:博客园
目录
  • 数组
  • 切片
    • 什么是切片?
    • 切片定义
    • 切片截取
    • append()
    • copy()
    • 切片总结
  • 字符串
    • 字符串常用函数
    • 字符串转换
  • Map(字典)
  • 结构体
    • 什么是结构体?
    • 定义与初始化
    • 结构体数组/切片
  • 指针
    • new()
    • 数组指针
    • 指针数组
    • 结构体指针变量
  • 值/引用传递 总结

数组

所谓的数组,是指数组是存放在连续内存空间上的相同类型数据的集合。

示例:数组定义和赋值

// 定义数组var arr [10]int  // 数组的长度定义只能用常量,且不能改变fmt.Println(len(arr))  // 打印数组长度// 定义时,元素已有默认值(基本数据类型的默认值)// 数组赋值// 方式一:繁琐arr[0] = 1arr[1] = 2...// 方式二:使用循环for i:=0; i

示例:数组初始化


【资料图】

// 全部初始化var arr [5]int = [5]int {1, 2, 3, 4, 5}// 部分初始化,其余保持默认值arr2 := [5]int {1, 2, 3}// 指定索引初始化arr3 := [5]int {2: 10, 3: 11}// 通过初始化再决定数组长度arr4 := [...]int {1, 2, 3, 4}

示例:数组作为实参

func modify(arr [5]int) {    arr[0] = 666    fmt.Println("arr after modify: ", arr)}func main() {    arr := [5]int{1, 2, 3, 4, 5}    modify(arr)  // [666 2 3 4 5]    fmt.Println("main arr: ", arr)  // [1 2 3 4 5]    // 注意结果:不会影响 main() 函数中数组 arr 的值}

在 GO 语言中,数组作为参数进行传递时是值传递,而切片作为参数进行传递时是引用传递

切片

什么是切片?

先思考一下数组有什么问题:

  1. 数组定义后,长度是固定的。
  2. 使用数组作为函数参数进行传递时,如果形参为 5 个元素的整型数组,那么实参也必须是 5 个元素的整型数组。

针对以上两个问题,可以使用切片(Slice)来进行解决。

与数组相比,切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以可以将切片理解成“动态数组”,但是它不是数组。

切片定义

// 方式一:定义空(nil)切片s1 := []int{}s2 := [...]int{}var s3 []int// 方式二:初始化切片s4 := []int {1, 2, 3}append(s4, 4, 5, 6)// 方式三:通过make函数定义s5 := make([]int, 5, 10)  // make(切片类型, 长度, 容量)// 容量参数也可以省略,此时容量=长度fmt.Println(s5)  // [0 0 0 0 0]// GO语言提供了相应的函数来获取切片的长度与容量fmt.Println("长度是:", len(s5))  // 5fmt.Println("容量是:", cap(s5))  // 10// len 是数组的长度,指的是这个数组在定义的时候,所约定的长度// cap 是数组的容量,指的是底层数组的长度,也可以说是原数组在内存中的长度// 切片同样可以使用下标或循环的方式赋值/取值// 但要注意循环结束条件不能大于切片的长度(而不是容量)

切片截取

所谓截取,就是从切片中获取指定的数据。

// 定义切片s := []int {10, 20, 30, 40, 50}// 窃取数据赋值给s1// s[low:high:max]:low表示开始截取的索引位,high表示结束截取的索引位(包头不包尾),max表示截取后的切片容量(cap=max-low)s1 := s[0:3:5]s2 := s[1:3:5]fmt.Println(s1)  // [10 20 30],容量为5fmt.Println(s2)  // [20 30],容量为4(s的容量减去s2的起始索引)

注意:修改新切片 s1/s2 的值时,会影响到原切片 s 的值,原因是新切片实际是指向了原切片(而不是新开辟内存空间)。

切片的相关操作:

操作含义
s[n]切片 s 中索引位置为 n 的项
s[:]从切片 s 的索引位置 0 到 len(s)-1 处所获得的切片
s[low:]从切片 s 的索引位置 low 到 len(s)-1 处所获得的切片
s[:high]从切片 s 的索引位置 0 到 high 处所获得的切片,len=high
s[low:high]从切片 s 的索引位置 low 到 high 处所获得的切片,len=high-low
s[low:high:max]从切片 s 的索引位置 low 到 high 处所获得的切片,len=high-low,cap=max-low
len(s)切片 s 的长度,总是 <= cap(s)
cap(s)切片 s 的容量,总是 >= len(s)

append()

s1 := []int{}// 通过append函数向切片末尾追加数据(增加容量)append(s1, 1, 2, 3)append(s1, 4)fmt.Println(s1)  // [1 2 3 4]

注意:当容量小于 1024 时按照 2 倍的容量扩容;当容量大于等于 1024 时按照小于 2 倍的容量扩容

copy()

func main() {    srcSlice := []int{1, 2}    dstSlice := []int{6, 6, 6, 6}    copy(dstSlice, srcSlice) // copy(目标切片, 源切片)    // 将srcSlice切片中两个元素拷贝到dstSlice元素中相同的位置,而dstSlice原有的元素被替换掉    fmt.Println("dst: ", dstSlice) // [1 2 6 6]    srcSlice1 := []int{1, 2}    dstSlice1 := []int{6, 6, 6, 6}    copy(srcSlice1, dstSlice1) // copy(目标切片, 源切片)    // 拷贝的长度为两个slice中长度较小的长度值    fmt.Println("dst: ", dstSlice) // [6 6]}

切片总结

Go 中的切片,是定义了新的指针,指向了原来数组所在的内存空间。所以,修改了切片数组的值,也就相应地修改了原数组的值。

此外,切片可以用 append 增加元素。但是,如果此时底层数组容量不够,切片将会指向一个【重新分配空间后进行拷贝的数组】。

因此可以得出结论:

  • 切片并不存储任何数据,它只是描述了底层数组中的一段。
  • 更改切片的元素会修改其底层数组中对应的元素。
  • 与它共享底层数组的切片都会观测到这些修改。

字符串

字符串常用函数

strings 包中常用的字符串处理函数:

函数说明
func Contains(s, substr string) bool判断字符串 s 中是否包含 substr,返回 bool 值
func Join(arr []string, sep string) string字符串连接,把 arr 中的元素通过 sep 拼接起来
func Index(s, sep string) int在字符串 s 中查找 sep 所在的位置,返回索引值,找不到返回 -1
func Repeat(s string,count int) string重复 s 字符串 count 次,返回重复的字符串
func Replace(s, old, news tring,n int) string在 s 字符串中,把 old 字符串替换为 new 字符串,n 表示替换的次数,小于 0 表示全部替换
func Split(s,sep string) []string把 s 字符串按照 sep 分割,返回 slice
func Trim(s string, cutset string) string在 s 字符串的头部和尾部,去除 cutset 指定的字符串
func Fields(s string) []string去除 s 字符串中的空格符,并且按照空格分割返回 slice

示例:

// Containsfmt.Println(strings.Contains("hellogo", "go")) // true// Joins := []string{"1", "2", "3"}buf := strings.Join(s, "|")fmt.Println(buf) // "1|2|3"// Repeatfmt.Println(strings.Repeat("go", 3)) // "gogogo"// Replacefmt.Println(strings.Replace("gogogo", "o", "d", -1)) // "gdgdgd"// Splitfmt.Println(strings.Split("hello@go@go@", "@")) // [hello go go ]  注意最后还有个空字符串元素// Trimfmt.Println(strings.Trim("   Are u ok?   ", " ")) // "Are u ok?"// Fieldsfmt.Println(strings.Fields("  Are u  ok  ?  ")) // [Are u ok ?]

字符串转换

GO 语言提供了字符串与其它类型之间相互转换的函数,相应的字符串转换函数都在 strconv 包中。

Format 系列函数:将其他类型转成字符串

// 整型转字符串fmt.Println(strconv.Itoa(666)) // "666"// 布尔转字符串fmt.Println(strconv.FormatBool(false)) // "false"// 浮点数转字符串// 3.14指需要转字符串的浮点数,"f"指打印格式,3指保留3位小数,64表示以float64处理fmt.Println(strconv.FormatFloat(3.14, "f", 3, 64))  // "3.140"

Parse 系列函数:将字符串转成其他类型

// 字符串转整数result, err := strconv.Atoi("666")if err != nil {    fmt.Println(result) // 666} else {    fmt.Println("转换失败,原因为:", err)}// 字符串转布尔fmt.Println(strconv.ParseBool("false")) // false // 字符串转浮点数fmt.Println(strconv.ParseFloat("123.12", 64)) // 123.12 

append():将整数等转换为字符串后,添加到现有的字节数组中

Map(字典)

  • Map 即 GO 语言中的字典(key-value)
  • key不能重复
  • 键的类型,必须是支持 == 或 != 操作符的类型(切片、函数以及包含切片的类型不能作为字典的键)
// 字典的定义dict := map[int]string  // []中指定的是键(key)的类型,后面紧跟着的是值(value)的类型fmt.Println(dict) // nilfmt.Println(len(dict)) // 0// 字典中不能使用 cap 函数,只能使用 len 函数。len 函数返回 map 拥有的键值对的数量// 也可以在定义时指定容量dict2 := make([int]string, 3)fmt.Println(len(dict2)) // 还是0,因为还没有赋值// 赋值dict2[3] = "张三"dict2[4] = "李四"dict2[5] = "王五"dict2[6] = "老六"// 容量不够时会自动扩容// 注意:map是无序的,我们无法决定它的返回顺序fmt.Println(dict2)fmt.Println(len(dict2)) // 4fmt.Println(dict2[6]) // 老六// 在定义时初始化dict3 := map[int]string{1:"mike", 2:"luke"}// 以循环的方式输出for key, value := range dict3 {    fmt.Printf("key=%d, value=%s", key, value)}// 输出时进行判断value, ok := dict3[1]  // ok表示key是否存在if ok == true {    fmt.Println(value)} else {    fmt.Println("key不存在")}

注意:map 作为函数参数时是引用传递

结构体

什么是结构体?

在 Go 中没有对象这一说法,因为 Go 是一个面向过程的语言。但是我们又知道面向对象在开发中的便捷性,所以在 Go 中有了结构体这一类型。

结构体是复合类型,当需要定义一种类型(它由一系列属性组成,每个属性都有自己的类型和值)时,就应该使用结构体,它把数据聚集在一起。

组成结构体类型的那些数据称为字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。

可以近似认为,一个结构体就是一个类,结构体内部的字段,就是类的属性。

注意,在结构体中也遵循用大小写来设置公有或私有的规则。如果这个结构体名字的第一个字母是大写,则可以被其他包访问,否则,只能在包内进行访问。而结构体内的字段也一样,也是遵循一样的大小写确定可用性的规则。

定义与初始化

结构体的定义方式如下:

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

示例:

// 定义结构体type Student struct {    id     int    name   string    score  int    gender byte    age    int}// type 后面跟着的是结构体的名字Student, struct表示定义的是一个结构体// 大括号中是结构体的成员,注意在定义结构体成员时,无需加varfunc main() {    // 顺序初始化:此时每个成员必须初始化,值的顺序与结构体成员的顺序保持一致    var student1 = Student{1, "小明", 97, "m", 18}    fmt.Println(student1) // {1 小明 97 109 18}    // 指定成员初始化。没有指定的成员自动赋默认值    var student2 = Student{id: 2, name: "小光"}    fmt.Println(student2) // {2 小光 0 0 0}    // 成员的使用    var student3 Student  // 也可以使用 student3 := new(Student)    student3.id = 3    student3.name = "小柳"    student3.gender = "f"    fmt.Println(student3) // {3 小柳 0 102 0}    // 结构体比较(这里比较的是值)    //两个结构体可以使用 == 或 != 运算符进行比较,但不支持 > 或 <    student4 := Student{4, "小二", 98, "m", 16}    student5 := Student{4, "小二", 98, "m", 16}    student6 := Student{6, "小六", 98, "m", 16}    fmt.Println(student4 == student5) // true    fmt.Println(student5 == student6) // false    // 同类型的两个结构体变量可以相互赋值    var student7 Student    student7 = student6    fmt.Println(student7) // {6 小六 98 109 16}}

注意:结构体作为函数参数时,是值传递

结构体数组/切片

// 定义结构体type Student struct {    id    int    name  string    score int}// 计算平均分func Avg(students []Student) int {    var sum int    for i := 0; i < len(students); i++ {        sum += students[i].score    }    return sum / len(students)}func main() {    // 定义并初始化结构体切片    students := []Student{        Student{101, "张三", 98},        Student{102, "李四", 66},        Student{103, "王五", 80},    }        fmt.Println("平均分=", Avg(students))  // 81}

指针

指针变量:用来存储任何一个值的内存地址

var num intfmt.Printf("num=%d\n", num)   // num=0,表示num的内存地址中存储的值fmt.Printf("&num=%v\n", &num) // &num=0x110160d8,表示num的内存地址// 定义一个指针变量(默认值为nil)var p *int  // 表示存储的是一个整型变量的地址p = &num    // 把变量num的内存地址赋值给指针变量pfmt.Printf("i=%d, p=%v\n", num, p) // i=0, p=0x110160d0// 根据存储的变量的地址,来操作变量的存储单元(如输出变量存储单元中的值、对值进行修改)*p = 80fmt.Printf("i=%d\n", num) // i=80

注意:普通变量作为函数参数进行传递时,是值传递;而指针作为参数进行传递时,是引用传递

new()

指针变量,除了以上介绍的指向以外(p=&a),还可以通过 new() 函数来指向。

var p *int// new(int) 作用就是创建一个整型大小的空间p = new(int)  // 也可以使用自动推导 p := new(int)// 然后让指针变量 p 指向了该空间*p = 59// 所以通过指针变量 p 进行赋值后,该空间中的值就是 59fmt.Println("*p=", *p)  // 59

new() 函数的作用就是 C 语言中的动态分配空间。但是在这里与 C 语言不同的地方,就是最后不需要关心该空间的释放,GO语言会自动释放。这也是比 C 语言使用方便的地方。

数组指针

数组作为函数参数进行传递时是值传递,如果想改为引用传递,可以使用数组指针(也就是让一个指针指向数组)。

// 定义一个数组,作为函数Swap的实参进行传递// 这里需要传递的是数组的地址,所以Swap的形参是数组指针func Swap(p *[3]int) {    (*p)[1] = 89  // 可以通过*p结合下标将对应的值取出来进行修改,注意要加小括号}func main() {    arr := [3]int{1, 2, 3}    // 这时指针p指向了数组arr,对指针p的操作实际上是对数组arr的操作    Swap(&arr)    fmt.Println(arr)  // [1 89 3]  发现arr的元素变化了}

指针数组

针数组指的是一个数组中存储的都是指针(也就是地址)。

// 定义指针数组var p [2]*inti := 10j := 20p[0] = &ip[1] = &jfmt.Println(p[0])  // 0x110140b0fmt.Println(*p[0]) // 10(不用加小括号,因为是先取p[0]的地址,再根据地址获取值)

结构体指针变量

前面定义了指针指向了数组,解决了数组引用传递的问题。那么指针是否可以指向结构体,也能够解决结构体引用传递的问题呢?大难是可以的。

type Student struct {    id    int    name  string    score int}func Test(p *Student) {    p.id = 19}func main() {    var p *Student = &Student{1, "xiaoming", 66} // 也可以使用 p := &Student{1, "xiaoming", 66}    fmt.Println(p)  // &{1 xiaoming 66}    fmt.Println(*p)  // {1 xiaoming 66}    Test(p)  // 传递的是结构体地址    fmt.Println(*p)  // {19 xiaoming 66}  发现id变化了}

值/引用传递 总结

默认传递:

  • 值传递:基本数据类型(包括字符串)、数组、结构体
  • 引用传递:切片、字典

若想将值传递改为引用传递:使用指针

关键词: