Go语言系列-Go内建容器

本文详细介绍了Go语言中的数组、切片、Map及rune类型。数组是值类型,长度固定,而切片是数组的视图,可变且可扩展。Map是无序的键值对集合,支持动态添加和删除元素。rune用于处理字符,特别是在UTF-8编码下。文章通过示例代码展示了各种操作和注意事项,帮助读者深入理解Go语言的基础语法。
摘要由CSDN通过智能技术生成

一、 数组

我们直接看一下Go语言中,数组的定义方法,代码如下:

package main

import "fmt"

func main() {
	// Go中,数组的定义需要明确长度
	// 定义一个五位的初始数组,默认每一位的值都为0
	var arr1 [5]int
	// 定义一个三位的数据,并赋初值1,3,5
	arr2 := [3]int{1, 3, 5}
	// 定义一个数组,赋初值2,4,6,8,10
	arr3 := [...]int{2, 4, 6, 8, 10}
	// 定义一个四行五列的初始二维数组
	var grid [4][5]int
	fmt.Println(arr1, arr2, arr3)
	fmt.Println(grid)
}

定义好数组后,我们最先想到的一个数组的处理方法,便是数组的遍历,下面的代码便能展示Go语言下,数组的遍历

func main() {
	// 省略部分前置代码

	arr3 := [...]int{2, 4, 6, 8, 10}

	// 数组遍历(最容易想到的)
	for i := 0; i < len(arr3); i++ {
		fmt.Println(arr3[i])
	}
	// Go中利用range来实现遍历
	// range关键字可以获得数组的下标
	for i := range arr3 {
		fmt.Println(arr3[i])
	}
	// 此外我们也可以获取到下标和对应的值
	for i, v := range arr3 {
		fmt.Println(i, v)
	}
	// 当然也可以只获得对应的值(可以通过_来省略变量,之前也有提到)
	for _, v := range arr3 {
		fmt.Println(v)
	}
}

Go语言下,遍历也可以通过关键字range很容易的实现出来

特别要注意的一点就是:数组是值类型,这也就意味着,定义数组的时候要明确长度,不同长度之间的数组会被认为是不同的类型;同时,在调用方法内对传入的数组修改,并不会影响方法外的数组,大家一起来看一下这段代码:

package main

import "fmt"

func printArray(arr [5]int)  {
	arr[0] = 50
	for i := range arr {
		fmt.Println(arr[i])
	}
}

func main() {
	// 省略部分前置代码
	var arr1 [5]int
	arr3 := [...]int{2, 4, 6, 8, 10}

	// 先打印arr1, arr3
	fmt.Println("arr1 arr3")
	fmt.Println(arr1, arr3)
	
	// 调用方法打印arr1
	fmt.Println("printArray(arr1)")
	printArray(arr1)

	// 调用方法打印arr3
	fmt.Println("printArray(arr3)")
	printArray(arr3)

	// 重新打印arr1, arr3
	fmt.Println("arr1 arr3")
	fmt.Println(arr1, arr3)

	// 大家也可以尝试调用方法打印一下arr2
	// printArray(arr2)

}

大家可以自行去打印一下先对应的结果,可能会更好的理解什么是值类型,打印结果如下,重点看图片中用红色框选出来的数字

在这里插入图片描述

所以,根据上述的打印结果,我们可以看到,原先的数组值其实并没有被修改,当然我们可以利用指针的方式,来进行对应的修改,但是会很麻烦,所以Go语言中,我们一般不直接使用数组,而是通过下面部分讲到的切片来进行操作

二、 切片(Slice)

敲黑板敲黑板,Go语言中,较为重要的一部分来了,它就是切片,我们先来看一下示例

	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	// 定义一个切片
	s := arr[2:6]
	fmt.Println(s)	// [2 3 4 5](左开右闭)
	// 一些切片省略写法
	fmt.Println("arr[2:6] = ", arr[2:6])
	fmt.Println("arr[:6] = ", arr[:6])
	fmt.Println("arr[2:] = ", arr[2:])
	fmt.Println("arr[:] = ", arr[:])

在Go语言中,Slice本身是没有数据的,它是对底层array的一个view,这就跟array的值类型不一样了,意味着,Slice本身是可以被修改的,看下面一个示例

package main

import "fmt"

func update(s []int) {
	s[0] = 100
}
func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	// 数组转切片
	s1 := arr[:]
	fmt.Println("BeforeUpdate arr = ", arr) // BeforeUpdate arr =  [0 1 2 3 4 5 6 7]
	fmt.Println("BeforeUpdate s1 = ", s1)   // BeforeUpdate s1 =  [0 1 2 3 4 5 6 7]
	update(s1)
	fmt.Println("AfterUpdate arr = ", arr)  // AfterUpdate arr =  [100 1 2 3 4 5 6 7]
	fmt.Println("AfterUpdate s1 = ", s1)    // AfterUpdate s1 =  [100 1 2 3 4 5 6 7]
}

这里,我们可以很明显的看到,数组的值再通过切片的update方法后,很轻易的就产生了改变,这样,我们就可以解决上面数组部分,只能利用指针来更新的那个麻烦方法,即把数组编程其对应的切片后在进行响应的操作

此外,GO语言中,可以在切片的基础上继续切片,这个被称为ReSlice,具体的代码如下:

	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	// 数组转切片
	s1 := arr[:]
	// ReSlice
	s1 := s1[2:]
	s1 := s1[:5]

A. Slice扩展

上面就是Slice切片的一些基础的用法,下面我们再来看一个示例,看看大家能不能够得到一个正确的答案:这里的s1和s2的值分别是什么,还是说会有报错

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := arr[2:6]
	s2 := s1[3:5]
	fmt.Println(s1) // [2 3 4 5]
	fmt.Println(s2) // [5 6]
}

打印的结果见注释,是不是很惊奇,s2的打印居然打印出了不输入s1的值,这个就是slice的扩展,具体就是slice在切片时,除了我们约定本身的一个len长度,还有一个从约定头到数组结尾的cap长度,当我们打印的切片长度超过len的时候,就回去对应cap的长度,这个就是Slice的扩展

注意:slice可以向后拓展,但是不可以向前拓展

具体规则:s[i]时,是不可以超过len(s),向后扩展时不可以超过底层数组cap(s);后半句就是我们上述代码展示的s2,前半句也很好理解,大家可以打印一个s1[4]即可,这里就不多描述了,直接上代码,给大家打印一下

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := arr[2:6]
	s2 := s1[3:5]
	fmt.Println(arr)
	fmt.Printf("s1 = %v, len(s1) = %d, cap(s1) = %d\n", s1, len(s1), cap(s1))
	fmt.Printf("s2 = %v, len(s2) = %d, cap(s2) = %d\n", s2, len(s2), cap(s2))
}

// 打印结果
arr= [0 1 2 3 4 5 6 7]
s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6
s2 = [5 6], len(s2) = 2, cap(s2) = 3

B. Slice添加元素

先来看下面这段代码,大家可以根据感觉先感受一下打印结果是什么

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := arr[2:6]
	s2 := s1[3:5]
	fmt.Println(arr) // [0 1 2 3 4 5 6 7]
	s3 := append(s2, 10)
	s4 := append(s3, 11)
	s5 := append(s4, 12)
	fmt.Println(s3)  // [5 6 10]
	fmt.Println(s4)  // [5 6 10 11]
	fmt.Println(s5)  // [5 6 10 11 12]
	fmt.Println(arr) // [0 1 2 3 4 5 6 10]
}

这个打印结果是不是出乎大家的意料,append(10),覆盖了7,然后append的11,12不见了

所以添加元素,在不超过cap的时候,会依次覆盖后续的原有元素;如果超过cap,系统会重新分配更大的底层数组来对应,也就是s4,s5不再是对arr的一个view;这时,如果arr在后续代码中不再使用的话,那么就会被垃圾回收掉

此外,由于值传递的关系,必须接收append的返回值,因为在append的时候,slice的ptr,len,cap都有可能改变,所以一般,s = append(s, val)

C. 创建Slice

上面的所有slice,我们都是通过一个数组来创建的;当然Go也可以直接创建一个Slice,如下:

// 不赋值
var s []int // Zero value for slice is nil
// 或者赋初值
s1 := []int{2, 4, 6, 8}
// 也可以通过make函数来创建 make(类型, len, cap)
s2 := make([]int, 10)
s2 := make([]int, 10, 32)

// 特别注意,slice的len可以为0或者任意正整数,但是在循环创建时cap一般为0和8的倍数

D. Sliceq其他操作

1) Slice还可以进行复制,代码如下

s1 := []int{2, 4, 6, 8}
s2 := make([]int, 10)
copy(s2, s1) // [2, 4, 6, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

2) Slice还可以删除其中摸个元素,代码如下

s2 = append(s2[:3], s2[4:]...) // 这样就删除掉了4号位元素 8
// 删除头
front := s2[0]
s2 = s2[1:]
// 删除尾
tail := s2[len(s2)-1]
s2 = s2[:len(s2)-1]

三、Map

上面说完了数组和切片,接下来我们继续来一种很常见的数据结构,那就是Map,先来看一下Map 的定义格式,如下:

// map[K]V
m := map[string]string {
		"name": "xxx",
		"course": "golang",
		"age": "25",
}

// map也可以复合,格式为map[K1]map[K2]V

// 此外,还可以利用make函数来创建,格式如下
m2 := make(map[string]int) // 这样子定义出来是一个空map
// m2 == empty map

// 当然我们也可以通过var关键字来进行定义
var m3 map[string]int // 这样子定义出来是一个空map
// m3 = nil

上面就是Go语言日常定义一个map的方式,接下来我们来看一下map的遍历,代码如下

func main() {
	m := map[string]string{
		"name":   "xxx",
		"course": "golang",
		"age":    "25",
	}

	// 想必下面这种形式的遍历,看到这里大家应该都不陌生了
	for k, v := range m {
		fmt.Println(k, v)
	}
}

大家可以打印一下这个map的遍历,多打印几遍,就会发现,每次打印kv的顺序是不一致的,也就是说,Go语言中的这个map是一个HashMap,是无序的

此外,针对于map的key,Go语言中map使用哈希表,所以要求必须可以比较相等,也就是除了slice,mp,function的内建类型都可以作为key

当自定义Struct类型不包含上述字段,也可以作为key

接下来我们来看一下map的常见操作

A. 获取Value

func main() {
	m := map[string]string{
		"name":   "xxx",
		"course": "golang",
		"age":    "25",
	}
	// 1. 获取value
	courseValue := m["course"]
	fmt.Println(courseValue) // golang
	// 特殊情况,当你输入key的时候不小心敲错了
	courseValue1 := m["cause"]
	fmt.Println(courseValue1) // 空(ZeroValue)
	// 应对上面的问题,可以如下处理
	courseValue2, ok := m["cause"]
	fmt.Println(courseValue2, ok) // 空(ZeroValue),false
	// 所以,日常使用可以拼接if来判断key是否存在
	if courseValue3, ok1 := m["cause"]; ok1 {
		fmt.Println(courseValue3)
	} else {
		fmt.Println("key does not exist in this map")
	}
}

B. 删除元素

delete(m, "name")

map的基本功能也就是这些了,因为map的用途非常广泛,在这里,我们用一道题来加深一下大家对于map的一个理解

例题:寻找最长不含有重复字符的字串(leetcode例题3)

在这里插入图片描述

题目如上所示,大家可以先思考一个思路

这个题目,我这里提供一种思路:滑动窗口,以供大家思考

我们分别定义一个左起点和右起点,左起点不懂,就指向第一个数,然后让右起点像右滑动,直到与左起点重复,记录窗口大小,然后左起点开始移动,同时随着左起点的移动,右起点继续滑动,并每次记录窗口大小,与之前的大小进行比对,直到窗口的大小,彻底小于最后记录的大小

完整的代码如下

package main

import "fmt"

//theme :无重复字符的最长子串

func lengthOfLongestSubstring(s string) int {
	// 哈希集合,记录每个字符是否出现过
	m := map[byte]int{}
	n := len(s)
	// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
	rk, ans := -1, 0
	for i := 0; i < n; i++ {
		if i != 0 {
			// 左指针向右移动一格,移除一个字符
			delete(m, s[i-1])
		}
		for rk + 1 < n && m[s[rk+1]] == 0 {
			// 不断地移动右指针
			m[s[rk+1]]++
			rk++
		}
		// 第 i 到 rk 个字符是一个极长的无重复字符子串
		ans = max(ans, rk - i + 1)
	}
	return ans
}

func max(x, y int) int {
	if x < y {
		return y
	}
	return x
}
/*
滑动窗口
在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,
然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。
在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。

复杂度:
时间复杂度:O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。
空间复杂度 :O(∣Σ∣),其中 \Σ 表示字符集(即字符串中可以出现的字符)

*/
func main()  {
	s := "bbbbb"
	result :=lengthOfLongestSubstring(s)
	fmt.Println(result)
}

大家感兴趣的话,可以尝试自己写一下,然后去leetcode提交一下,这道题就到这里,下面我们来看一个前面提到的很重要的一个数据类型

四、 rune

rune这个类型,就相当于其他语言当中的char类型,它可以很好地帮助我们来处理中文这些字符,老样子,我们还是来看代码

package main

import "fmt"

func main() {
	s := "Oh就要放假了yeah!!"
	fmt.Println(s) // Oh就要放假了yeah!!
	// 获取字符数量
	fmt.Println(utf8.RuneCountInString(s))	// 13
	// 这里我们来看一下它的具体长度
	fmt.Println(len(s)) // 27
	// 具体来拆解一下这个长度的计算
	fmt.Printf("%X\n", []byte(s)) // 4F68E5B0B1E8A681E694BEE58187E4BA8679656168EFBC81EFBC81
	// 为了方便查看,我们循环打印一下
	for _, b := range []byte(s) {
		fmt.Printf("%X ", b) // 4F 68 E5 B0 B1 E8 A6 81 E6 94 BE E5 81 87 E4 BA 86 79 65 61 68 EF BC 81 EF BC 81
	}
	fmt.Println()
}

上面的长度是不是很出乎我们的意料,所以我们循环按照ASCII码来进行了打印,这里我们主要看最后六个打印结果“EF BC 81 EF BC 81”,它对应了我们原始s中的两个中文感叹号,这也就是说,在这里,一个中文代表了三个字符,这样我们再来计算一下,‘就要放假了’+‘!!’为3×7=21个字符,再加上6个英文字符,一共就是27个字符,这就和我们的打印len的长度一致了

这就是UTF-8编码,它是一种可变长度的编码,这种编码下,英文为1字节,中文为3字节

如果还不能明显,大家可以按照下面去打印,注意看各个值打印的角标,会发现,中文部分,角标是按照+3的形式跳的,也说明了,中文是占了三个字节

// 打印对应的角标和对应Unicode编码
	for i, ch := range s { // ch is a rune
		fmt.Printf("(%d %X)", i ,ch)
	}
// (0 4F)(1 68)(2 5C31)(5 8981)(8 653E)(11 5047)(14 4E86)(17 79)(18 65)(19 61)(20 68)(21 FF01)(24 FF01)

这样,解释清楚了这块,我们看这个角标,因为它是跳跃的,如果在取值的时候,不小心取错了角标,就有可能产生错误,而我们在处理数据的时候,还是会希望,能够取到第几位的内容,故我们可以按照下方的代码来处理

	// 将整串转换为rune格式,再进行遍历
	for i, ch := range []rune(s) {
		fmt.Printf("(%d %c)", i, ch)
	}
	// (0 O)(1 h)(2 就)(3 要)(4 放)(5 假)(6 了)(7 y)(8 e)(9 a)(10 h)(11 !)(12 !)
	fmt.Println()

五、 字符串操作

  • Fields,Split, Join
  • Contains, Index
  • ToLower, ToUpper
  • Trim, TrimRight, TrimLeft

至此为止,关于Go语言基础的语法部分,我们就告一段落了,后面就来看一些Go语言进阶很重要的东西

本文至此便告一段落,祝各位码上无ERROR,键盘无BUG!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值