go基础部分二 | 青训营笔记

114 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

今天和大家分享go基础的第二部分。主要包括切片,map,结构体和指针的概念和常见操作。

切片

切片是对数组的一个连续片段的引用,所以切片是一个引用类型,sllice会将数组作为其底层的数据结构,只是没有固定长度。

声明一个切片:

var slice []int //声明整形切片
var numListEmpty []int{} //声明一个空切片//make函数构造一个切片
numList := make([]int, 3, 5) //表明类型是int型数组,后两个参数切片的长度,切片的容量
// 此时底层会开辟一段数组内存,其长度为5,但是可以使用的只有前三个元素,要使用后面两个需要进行扩容操作,否则会panic// 从数组中截取切片
arr := [5]int{1, 2, 3, 4, 5}
slcie := arr[2:4] //若是[:]则代表全部的元素
slice2 := arr[2:4:2] //表示切片取数组[2,4), 其容量为2,否则其容量会根据底层数组的截取大小设置为3
fmt.Println(slcie)
//[3 4] 

若没有对引用类型进行赋值,那么其值默认为nil

切片的扩容:

slice := []int{1, 2} // 此时切片的长度和容量相同,都是2
slice = append(slice, 1) // 我们使用追加函数,由于元素被追加进来时没有多余的空间了,所以进行扩容操作
fmt.Println(slice) // 扩容后的切片的容量为4,长度为3
//[1 2 1]

这里重点说一下扩容时的底层操作:

  • 将原切片的值复制
  • 生成一个原切片值两倍容量的底层数组,并将切片和追加的元素放进去
  • 返回数组的引用

可以看到扩容时并不是在原有数组上进行操作,而是重新生成了一个长度为数组两倍的数组作为新的底层数组,其地址已经不同了。所以我们可以说扩容后的切片已经不等于原切片了。我们可以进行以下实验:

func main() {
    a1 := []string{"1", "2"}
    a2 := append(a1, "3") // 进行扩容,扩容后的底层数组长度为4,即切片容量为4
    a3 := append(a2, "4") // 不需要扩容
    a3[0] = "3"
    fmt.Println(a1, a2, a3) // [1 2] [3 2 3] [3 2 3 4]
}

可以看到,当改变a3时,a2变化了,a1没有变化,说明a1的底层数组和a2,a3已经不是同一个引用了。

对切片的复制

a := []int{1, 2}
b := []int{3}
copy(a, b)
fmt.Println(a)
// 3 2
copy(a[1:], b)  // 可以指定从确定下标开始向a复制b的值
fmt.Println(a)
// 3 3

在函数中使用切片:

func main() {
    s := []int{1, 2, 3} // 创建了底层数组并创建一个切片指向它
    func(myStr []int) {
        myStr[1] = 3 // myStr是对s的复制,但同时两者都指向一个底层数组
    }(s)
    fmt.Println(s) // [1 3 3] 说明改变myStr其实就是改变底层数组,相当于也影响了s
}

利用方法和切片实现各种功能(例如排序)

我们可以在Go语言中声明底层为切片或者数组的类型,并为其绑定相应的方法。跟其他语言的类(class)相比,Go语言在类型之上声明方法的能力无疑更为通用。

例如标准库的sort包声明了StringSlice类型:type StringSlice []String并且该类型还关联了方法:func (p StringSlice) sort()

为了按照字母顺序对某一个切片排序,我们就可以利用上面的sort方法:

func main() {
    s := []string{"a", "c", "b"} // 创建了底层数组并创建一个切片指向它
    sort.StringSlice(s).Sort()   // 将s变为StringSlice类型之后进行排序  这里sort包提供了辅助函数简化操作:sort.Stringss(s)
    fmt.Println(s)               // a b c
}

Map

通过字面值创建Map:

mapOne := map[string]string{
        "a": "A",
        "b": "B",
    }
fmt.Println(mapOne)
//map[a:A b:B]

通过make创建Map:

mapTwo := make(map[string]string)
mapTwo["a"] = "cat"
fmt.Println(mapTwo)
// map[a:cat]

注意map是无序的,并且map是一个引用类型

添加和删除map,判断是否存在某个键值对:

mapOne := map[string]string{
  "a": "A",
  "b": "B",
}
mapOne["c"] = "C" //插入
delete(mapOne, "a")
fmt.Println(mapOne)
value, ok := mapOne["c"]
fmt.Printf("%s %t\n", value, ok)
/*map[b:B c:C]
C true*/

虽然map是无序的,但是我们仍然可以进行遍历map,我们可以使用for range语法:

for key, value := range mapOne {
        fmt.Println(key, value)
    }
/*
c C
b B
*/

更多range的参考可以查看:www.runoob.com/go/go-range…

结构体

声明一个结构体

type Person struct {
    name   string //名称
    age    int    //年龄
    target string //目标
    behave string //行为
}

我们在使用的时候就可以采用

xiaoming := Person{
        name:   "xiaoming",
        age:    10,
        target: "xx",
        behave: "yy",
    }
    println(xiaoming.name)

同时,我们也可以这样:

xiaoming := Person{"xiaoming", 19, "xx", "yy"} //必须按照顺序

我们还可以先声明该结构体的变量后进行赋值:

type Person struct {
    name   string //名称
    age    int    //年龄
    target string //目标
    behave string //行为
}
xiaoming := Person{}
xiaoming.name = "123"

匿名结构体

    xiaoming := struct {
        name   string //名称
        age    int    //年龄
        target string //目标
        behave string //行为
    }{
        name:   "xiaoming",
        age:    10,
        target: "xx",
        behave: "yy",
    }
    fmt.Println(xiaoming) //{xiaoming 10 xx yy}

结构体指针

结构体声明的变量指针比较特殊,它可以直接代替变量去访问属性:

type Person struct {
        name   string //名称
        age    int    //年龄
        target string //目标
        behave string //行为
    }
​
    ptr := &Person{
        name:   "xiaoming",
        age:    18,
        target: "xx",
        behave: "yy",
    }
    fmt.Println((*ptr).age, ptr.age) // 18 18

这里其实是统一了C语言中的指针使用->访问属性和变量使用.访问属性的形式,统一使用.

结构体转发

结构体的嵌套和字段提升:

    type Other struct {
        x int
        y int
    }
    type Person struct {
        name  string
        age   int
        other Other
    }
    s := Person{
        name: "xiaoming",
        age:  18,
        other: Other{
            x: 1,
            y: 2,
        },
    }
    fmt.Println(s.other.x) // 这里我们访问x需要先访问other

此时,我们若将Other定义为匿名字段的类型,则会出现字段提升,我们访问Other内部的字段可以直接使用Person,这种特性称为转发:

    type Other struct {
        x int
        y int
    }
    type Person struct {
        name string
        age  int
        Other
    }
    s := Person{
        name: "xiaoming",
        age:  18,
        Other: Other{
            x: 1,
            y: 2,
        },
    }
    fmt.Println(s.x) //注意我们这里使用的是s.x,不必再去访问Other 

同时,这种匿名字段也可以实现对方法的转发,给结构体添加方法:

type Person struct {
    name string
    age  int
}
​
func (p Person) ShowFullName(str string) (fullName string) {
    fullName = str + p.name
  return
}
​
func main() {
    s := new(Person)
    s.name = "ming"
    name := s.ShowFullName("xiao")
    fmt.Println(name) // xiao ming
}

指针

创建一个指针:

func main() {
    str := "hello"
    ptr := &str
    fmt.Println(ptr, *ptr)
}
//0xc000010250 hello

注意,go语言中的指针不支持例如ptr++这样的运算(只能使用ptr + 1来找到指针后的地址)

www.go-edu.cn/2022/05/08/…

解引用

指针为数组和结构体提供了可以自动解引用的操作,即(&arr)[0]可以直接使用arr[0]代替:

arr := [...]int{1, 2}
arrPointer := &arr
fmt.Println(arr[0], arrPointer[0]) // 1 1

需要注意,虽然go给数组提供了自动解引用的操作,但是并没有为切片和映射提供自动解引用的特性。