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

posted @ 2020-04-08 01:26  瓜落桥下塘  阅读(176)  评论(0编辑  收藏  举报