Go语言基础(三)
一.切片类型
1.什么是切片
切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper)。切片本身不拥有任何数据。它们只是对现有数组的引用
2.切片的第一种方式:用数组创建一个切片
// 切片定义的第一种方式:由数组切出来 var a [5]int= [5]int{1,2,3} // 切片是对数组的引用 var b []int=a[1:4] // 数组的切片没有步长 fmt.Println(b) //a[2] = 30 // 底层数组会影响切片 //fmt.Println(b) b[0] = 20 // 改变切片会影响底层数组 fmt.Println(a)
修改切片的值会影响到底层数组,并且修改底层数组也会影响到切片
切片的空值是nil类型
// 切片的空值是nil类型 var a[]int if a == nil{ fmt.Println("nil类型") } a[0] = 10 // 空值也不能赋值 fmt.Println(a)
3.切片的第二种方式:用make创建一个切片
func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度。make 函数创建一个数组,并返回引用该数组的切片。
// 第二种方式:直接定义,第一个数字是切片的大小,第二个数字是底层数组的长度,也就是切片的容量 var a[]int = make([]int,4,5) fmt.Println(a)
4.切片的长度(len)和容量(cap)
切片追加值与查看长度和容量
// 切片的长度(len)和容量(cap) var a [5]int= [5]int{1,2,3,4,5} var b []int=a[1:4] // 切片追加值 b = append(b,999) // 长度加1,容量不变 fmt.Println(b) fmt.Println(a) // 底层数组也会跟着改变 fmt.Println(len(b)) // 长度4 fmt.Println(cap(b)) // 容量4
当切片追加超过容量的值时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。并且这个新切片的容量是原来切片的两倍
// 切片追加超过容量的值 b = append(b,888) fmt.Println(len(b)) // 长度5,超过了容量 fmt.Println(cap(b)) // 容量8,拷贝了原容量数组,并生成了一个原容量两倍的新数组 b[0] = 777 fmt.Println(b) fmt.Println(a) // 此时修改切片不会影响底层数组,他们间的关系断了,反之亦然,修改数组也不会英雄切片
5.切片的函数传递
我们可以认为,切片在内部可由一个结构体类型表示。
切片的数据结构展示:
type slice struct { Length int Capacity int ZerothElement *byte }
切片包含长度、容量和指向数组第零个元素的指针。当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。因此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见。让我们写一个程序来检查这点。
// 切片的函数传递 a := make([]int,5) test(a) fmt.Println(a)
test函数
func test(a []int) { a[0] = 999 fmt.Println(a) }
当我们在函数中修改切片的值时,外部的切片也会随之更改
6.多维切片
类似于数组,切片可以有多个维度。
// 多维切片 var a[][]string = make([][]string,3) fmt.Println(a) // 长度为3的切片,有三个空值的切片元素 a[0] = make([]string,2) a[0][0] = "sxc" a[0][1] = "age" fmt.Println(a) // 长度为3的切片,并且第一个元素是一个长度为2的切片
循环多维数组
// 循环多维切片 var a = [][]string{{"sxc","cool"},{"zzj","dsb"}} //fmt.Println(a) for _,v := range a{ for _,v1 := range v{ // 循环两次 fmt.Println(v1) } }
7.切片的第三种方式:切片初始化时直接赋值
// 第三种方式:切片初始化,直接赋值 var a[]int = []int{1,2,3} fmt.Println(a) fmt.Println(len(a)) // 长度为3 fmt.Println(cap(a)) // 容量也为3
多维数组初始化时直接赋值
// 多维切片初始化 var a[][]string = [][]string{{"sxc","cool"},{"zzj","dsb"}} fmt.Println(a) a[0][0] = "zzp" fmt.Println(a)
8.切片修改值只跟切片的长度有关,跟容量无关
只能修改长度以内的切片
// 修改值只跟长度有关 var a[]int = []int{1,2} a[0] = 10 fmt.Println(a) //a[2] = 30 // 超过长度,不能修改 a = append(a, 30) a[2] = 50 fmt.Println(a) // 可以修改
9.内存优化
切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。
一种解决方法是使用 [copy] 函数 func copy(dst,src[]T)int
来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收。
// 内存优化,copy函数 var a[]int = make([]int,3,10000) a[0] = 5 a[1] = 6 fmt.Println(a) //b:= make([]int,2,6) // 当长度少时,只取前几个 b:= make([]int,4,6) // 当长度多时,后面用0补充 copy(b, a) fmt.Println(b)
如上述切片,他的长度只有三,但是他引用的底层数组长度却有10000,此时使用该切片就十分耗费内存,我们可以使用copy函数来进行内存优化
二.可变参数函数
可变参数函数是一种参数个数可变的函数。
语法:
如果函数最后一个参数被记作 ...T
,这时函数可以接受任意个 T
类型参数作为最后一个参数。
请注意只有函数的最后一个参数才允许是可变的。
func main() { var a = []int{1,2,3,4} //test1(1,2,3,4) test1(a...) // 相当于打散 } func test1(a ...int) { fmt.Println(a) }
三.Map
1.什么是map
map 是在 Go 中将值(value)与键(key)关联的内置类型。通过相应的键可以获取到值。类似于python中的字典
2.如何创建map
//语法 // map类型的key必须可hash var a map[key类型]value值类型 var a map[int]string fmt.Println(a) // map也是一个引用类型,空值也是nil
3.第一种创建方式:使用make初始化
// map使用make初始化 var a = make(map[int]string) a[0] = "sxc" fmt.Println(a[0]) // 取不存在的值会返回value类型的空值 fmt.Println(a[1])
map的空值,就是value对应的类型的空值,比如int就是0
// 判断空值 if a[1]==""{ // 字符串的空值就是空字符串 fmt.Println("空值") } //可以使用该方法来判断是否为空值,因为每个类型的的空值的类型都是不一样的 if v,ok := a[1];ok{ fmt.Println(v) }else{ fmt.Println("空值") }
3.第二种创建方式:直接赋值
// 初始化第二种方式,直接赋值 var a = map[int]string{0:"sxc",1:"zzj"} fmt.Println(a)
4.map删除值:delete方法
删除 map
中 key
的语法是 [delete(map, key)]。这个函数没有返回值。
// map删除值,内置函数delete delete(a, 1) fmt.Println(a)
5.获取map的长度
获取 map 的长度使用 [len]函数。
// map长度,len
fmt.Println(len(a))
6.map是引用类型
和 [slices]类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
// map是引用类型,函数中修改也会影响
test5(a)
fmt.Println(a)
test函数中修改值,原值也会跟着改变
func test5(a map[int]string) { a[0] = "zzp" }
7.map的相等性
map 之间不能使用 ==
操作符判断,==
只能用来检查 map 是否为 nil
。
判断两个 map 是否相等的方法是遍历比较两个 map 中的每个元素。
8.扩展
map是无序的,可以通过逻辑设计成有序的
m := make(map[int]string) var l []int m,l = add(1,"sxc", m, l) m,l = add(2,"zzj", m, l) m,l = add(3,"zzp", m, l) m,l = add(4,"lzx", m, l) m,l = add(5,"yzy", m, l) fmt.Println(m) fmt.Println(l) for _,v := range l{ fmt.Println(m[v]) }
func add(b int, c string,m map[int]string,l []int) (map[int]string,[]int){ m[b] = c l = append(l,b) return m,l }
四.字符串
1.什么是字符串
Go 语言中的字符串是一个字节切片。把内容放在双引号""之间,我们可以创建一个字符串。让我们来看一个创建并打印字符串的简单示例。
package main import ( "fmt" ) func main() { name := "Hello World" fmt.Println(name) }
Go 中的字符串是兼容 Unicode 编码的,并且使用 UTF-8 进行编码
2.字符串的字节数
name := "hello 你好" fmt.Println(len(name)) // 统计字节数,12
上述代码中的name由5个英文字符(5*1共5个字节),一个空格(1个字节),两个中文字符组成(2*3共6个字符),故字节数是12
循环获取每个字节代码的十进制数字,都是unit8类型,也就是byte类型
// 字符串的循环,字符串是个只读切片 name := "hello你" for i:=0;i<len(name);i++{ fmt.Println(name[i]) fmt.Printf("%T",name[i]) // uint8也就是byte类型 fmt.Println() }
3.字符串的字符数
name := "hello 你好" fmt.Println(utf8.RuneCountInString(name)) // 统计字符数
上述代码中的name一共由8个字符组成,故字符数是8
循环获取每个字符代码的十进制数字,都是int32类型,也就是rune类型
for _,v := range name{ fmt.Println(v) fmt.Printf("%T",v) // rune也就是int32类型 fmt.Println() fmt.Println(string(v)) }
4.字符串的长度
[utf8 package] 包中的 func RuneCountInString(s string) (n int)
方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。
package main import ( "fmt" "unicode/utf8" ) func length(s string) { fmt.Printf("length of %s is %d\n", s, utf8.RuneCountInString(s)) } func main() { word1 := "Señor" length(word1) word2 := "Pets" length(word2) }
上面程序的输出结果是:
length of Señor is 5 length of Pets is 4
使用切片的方法拿值
fmt.Println(string(name[6])) // 只能拿字符的字节数对应的值,所以中文字符取不到
5.字符串是不可变的
Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。
package main import ( "fmt" ) func mutate(s string)string { s[0] = 'a'//any valid unicode character within single quote is a rune return s } func main() { h := "hello" fmt.Println(mutate(h)) }
在上面程序中的第 8 行,我们试图把这个字符串中的第一个字符修改为 'a'。由于字符串是不可变的,因此这个操作是非法的。所以程序抛出了一个错误 main.go:8: cannot assign to s[0]。
为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。
package main import ( "fmt" ) func mutate(s []rune) string { s[0] = 'a' return string(s) } func main() { h := "hello" fmt.Println(mutate([]rune(h))) }
在上面程序的第 7 行,mutate
函数接收一个 rune 切片参数,它将切片的第一个元素修改为 'a'
,然后将 rune 切片转化为字符串,并返回该字符串。程序的第 13 行调用了该函数。我们把 h
转化为一个 rune 切片,并传递给了 mutate
。这个程序输出 aello
。
五.指针
1.什么是指针
指针是一种存储变量内存地址(Memory Address)的变量。
如上图所示,变量 b
的值为 156
,而 b
的内存地址为 0x1040a124
。变量 a
存储了 b
的地址。我们就称 a
指向了 b
。
2.指针的声明(*+变量的类型)
指针变量的类型为 *T,该指针指向一个 T 类型的变量。
package main import ( "fmt" ) func main() { b := 255 var a *int = &b fmt.Printf("Type of a is %T\n", a) fmt.Println("address of b is", a) }
& 操作符用于获取变量的地址。上面程序的第 9 行我们把 b
的地址赋值给 *int 类型的 a
。我们称 a
指向了 b
。当我们打印 a
的值时,会打印出 b
的地址。程序将输出:
Type of a is *int address of b is 0x1040a124
3.指针的零值
指针的零值是nil
package main import ( "fmt" ) func main() { a := 25 var b *int if b == nil { fmt.Println("b is", b) b = &a fmt.Println("b after initialization is", b) } }
上面的程序中,b
初始化为 nil
,接着将 a
的地址赋值给 b
。程序会输出:
b is <nil> b after initialisation is 0x1040a124
4.指针的解引用,反解(*+变量)
指针的解引用可以获取指针所指向的变量的值。将 a
解引用的语法是 *a
。
package main import ( "fmt" ) func main() { b := 255 a := &b fmt.Println("address of b is", a) fmt.Println("value of b is", *a) }
在上面程序的第 10 行,我们将 a
解引用,并打印了它的值。不出所料,我们会打印出 b
的值。程序会输出:
address of b is 0x1040a124 value of b is 255
我们再编写一个程序,用指针来修改 b 的值。
package main import ( "fmt" ) func main() { b := 255 a := &b fmt.Println("address of b is", a) fmt.Println("value of b is", *a) *a++ fmt.Println("new value of b is", b) }
在上面程序的第 12 行中,我们把 a
指向的值加 1,由于 a
指向了 b
,因此 b
的值也发生了同样的改变。于是 b
的值变为 256。程序会输出:
address of b is 0x1040a124 value of b is 255 new value of b is 256
5.向函数传递指针参数
package main import ( "fmt" ) func change(val *int) { *val = 55 } func main() { a := 58 fmt.Println("value of a before function call is",a) b := &a change(b) fmt.Println("value of a after function call is", a) }
在上面程序中的第 14 行,我们向函数 change
传递了指针变量 b
,而 b
存储了 a
的地址。程序的第 8 行在 change
函数内使用解引用,修改了 a 的值。该程序会输出:
value of a before function call is 58 value of a after function call is 55
6.不要向函数传递数组的指针,而应该使用切片
假如我们想要在函数内修改一个数组,并希望调用函数的地方也能得到修改后的数组,一种解决方案是把一个指向数组的指针传递给这个函数。
package main import ( "fmt" ) func modify(arr *[3]int) { (*arr)[0] = 90 } func main() { a := [3]int{89, 90, 91} modify(&a) fmt.Println(a) }
在上面程序的第 13 行中,我们将数组的地址传递给了 modify
函数。在第 8 行,我们在 modify
函数里把 arr
解引用,并将 90
赋值给这个数组的第一个元素。程序会输出 [90 90 91]
。
**a[x] 是 (*a)[x] 的简写形式,因此上面代码中的 (*arr)[0] 可以替换为 arr[0]**。下面我们用简写形式重写以上代码。
package main import ( "fmt" ) func modify(arr *[3]int) { arr[0] = 90 } func main() { a := [3]int{89, 90, 91} modify(&a) fmt.Println(a) }
该程序也会输出 [90 90 91]
。
这种方式向函数传递一个数组指针参数,并在函数内修改数组。尽管它是有效的,但却不是 Go 语言惯用的实现方式。我们最好使用切片来处理。
接下来我们用[切片]来重写之前的代码。
package main import ( "fmt" ) func modify(sls []int) { sls[0] = 90 } func main() { a := [3]int{89, 90, 91} modify(a[:]) fmt.Println(a) }
在上面程序的第 13 行,我们将一个切片传递给了 modify
函数。在 modify
函数中,我们把切片的第一个元素修改为 90
。程序也会输出 [90 90 91]
。所以别再传递数组指针了,而是使用切片吧。上面的代码更加简洁,也更符合 Go 语言的习惯。
7.Go不支持指针运算
Go 并不支持其他语言(例如 C)中的指针运算。
package main func main() { b := [...]int{109, 110, 111} p := &b p++ }
上面的程序会抛出编译错误:main.go:6: invalid operation: p++ (non-numeric type *[3]int)。
104