五、集合
array
数组
- 数组存放的是固定长度、相同类型的数据,连续存放
- 声明数组:
arr := [5]string{"a", "b", "c", "d", "e"} - [5]string和[6]string,不是同一类型,即长度也是数组类型的一部分
- 下标从 0 开始,访问元素:arr[0]
- 定义数组时长度可以忽略,go会自动推导出长度:arr:=[...]string{"a", "b"}
- 但是以上省略长度的写法,只适用于所有元素全部初始化的方式 如下的:arr := [5]string{1:"a", 3:"c"}表示,初始化下标1的值为"a",下标3的值为"c",数组长度为5,则其他位置会补“零值” 但是如果省略长度的话,整个数组的长度只有4
arr := [...]string{1: "a", 3: "c"}
fmt.Println(arr) // [ , a, , c] 没有初始化的默认是空值
/*
循环遍历数组
*/
for i := 0; i < len(arr); i++ {
fmt.Print(arr[i], " ")
}
// for range 循环遍历数组
for i, v := range arr {
fmt.Printf("数组索引:%d, 值是:%s\n", i, v)
}
// 如果返回值用不到,可以使用下划线_代替,直接丢弃
for _, v := range arr {
fmt.Println(v)
}
slice
切片
-
切片和数组类型,可以理解为动态数组,是基于数组实现的,对数组分割,得到切片
-
slice := arr[st:ed],st:ed是下标,是左闭右开的
-
切片是具备三个字段的数据结构:data | len | cap
data:指向数组的指针 len:长度 cap:容量
-
小技巧,st和ed都是可以省略的,
arr[:3] = arr[0:3] arr[1:] = arr[1:5] arr[:] = arr[0:5]
-
切片修改,可以修改切片中的元素,发现被引用的数组也被修改了。进一步证明,切片的底层是数组
-
切片声明:有三种方式
6.1 切割数组 slice := arr[2:5] 6.2 make函数声明 slice := make([]string, len, cap) len是切片长度,cap:切片容量 声明时,切片容量cap可以省略,切片容量不能小于切片长度 6.3 通过字面量方式初始化(和声明数组很类似) slice := []int{1, 2, 3} 此时长度 = 容量
-
append函数,对切片追加元素 append 会自动处理切片容量不足需要扩容的问题。
7.1 追加一个元素 slice2 := append(slice1, "F") 7.2 追加多个元素 slice2 := append(slice1, "E", "F") 7.3 追加另一个切片 slice2 := append(slice1, slice3....) 小技巧:在创建新切片的时候,最好要让新切片的长度和容量一样, 这样在追加操作的时候就会生成新的底层数组,从而和原有数组分离, 就不会因为共用底层数组导致修改内容的时候影响多个切片。
-
for range 循环遍历切片
-
因为切片效率高,一般都用切片
arr := [...]int{1, 2, 3, 4, 5}
sli := arr[1:4]
fmt.Println(sli) // [2, 3, 4]
// 循环遍历切片
for _, v := range sli {
fmt.Println(v)
}
map
-
map是无序的K-V键值对集合,所有的key必须为同一类型,所有的value也必须是同一类型。map[K]V
-
key的类型必须要支持 == 运算符,这样才能判断元素是否存在,并且保证key的唯一
-
声明map
3.1 make函数 mp := make(map[string]int) 声明了 key类型为string,value类型为int的map 3.2 通过字面量初始化 mp := map[string]int{"赛涵", 20}
-
添加键值对
mp["赛涵"] = 21 key是唯一的,如果key已经存在,则更新value的值
-
map的获取
a := mp["赛涵"] // 通过key来获取value的值
map可以获取不存在的K-V键值对,如果key不存在,则返回value类型的“零值” map的[] 返回两个值,1.对应的value,2.bool值,标志key是否存在
-
删除键值对
使用delete函数:delete(mp, Key) 第一个参数是map,第二个参数是要删除的K-V对的Key
-
遍历map
使用 for range 遍历,有两个返回值:Key和Value 注意:
- map的遍历是无序的,每次遍历的顺序可能会不同
- for range map 时可以只要一个返回值,默认是key
-
map的大小
map只有长度,没有容量,可以一直添加元素
// 声明
mp := make(map[string]int)
mp["赛涵"] = 20
a := mp["赛涵"]
fmt.Println(a)
// 取出两个值:value、是否存在
age, ok := mp["saihan"]
if ok {
fmt.Println(age)
} else {
fmt.Println("不存在")
}
// 如果key已存在:赋值,key不存在:添加
mp["saihan"] = 18
mp["Changersh"] = 20
// for range 遍历
for k, v := range mp {
fmt.Println(k, v)
}
六、函数与方法
函数
函数:
-
函数声明
func funcName(params) result{}1.1 params 是定义形参的变量名、类型,有0~多个参数 1.2 result是函数的返回值,用于定义返回值的类型,如果没有返回值,则可以省略 1.3 如果参数类型都是一样的,可以只写最后一个的参数类型
-
多值返回
go支持多个返回值
-
命名返回参数
可以给函数的返回值命名和类型,这个名字可以和参数一样在函数体内使用 都进行命名之后,就可以忽略return之后的返回值了,直接只写一个return即可
-
可变参数
func name(params...int)(){} 在参数类型前面加三个点...即可
-
包级函数
同一个包中的函数,哪怕是私有的,也可被调用,不同包中的函数如果要被调用,必须是公有的
5.1 函数名称首字母大写,代表公有函数,不同的包可以调用 5.2 函数名称首字母小写,代表私有函数,只有在同一个包中才可调用 5.3 任何一个函数都会属于某个包
-
匿名函数和闭包
- 匿名函数就是没有名字的函数 fun main(){ sum := func(a, b int) int{ return a + b } } 上面这个就是匿名函数,sum是数据类型为函数类型的变量,并不是函数名 通过sum,我们可以对匿名函数进行调用 sum(1, 2)
- 闭包 通过匿名函数,我们可以进行函数嵌套
func main() {
// 调用函数
fmt.Println(sum(5, 10))
fmt.Println(sum1(5, -6))
fmt.Println(sum2(3, 4, 5, 5, 7, 8, 2, 5))
// 函数嵌套
cl := closure()
fmt.Println(cl()) // 1
fmt.Println(cl()) // 2
fmt.Println(cl()) // 3
}
func sum(a int, b int) int {
// 计算两数之和
return a + b
}
// 测试多个返回值
func sum1(a, b int) (int, error) {
// 要求 a b都要是非负数,否则有异常
if a < 0 || b < 0 {
return 0, errors.New("a或者b不能为负数")
}
return a + b, nil
}
// 可变参数 + 返回值命名
func sum2(params ...int) (sum int, err error) {
for a := range params {
if a < 0 {
sum = 0
err = errors.New("不能为负值")
return
}
sum += a
}
// 返回值命名后,函数内可以使用
// 返回值只写 return即可
return
}
/*
函数closure返回了一个匿名函数,该匿名函数也被称为闭包。
闭包函数内部定义了变量i并初始化为0。然后,闭包函数返回另一个匿名函数,该函数会递增变量i的值,并将其作为结果返回。
闭包函数通过捕获周围作用域内的变量(在本例中是变量i),使得在每次调用闭包函数时,可以访问和修改该变量的值,并保留状态。
这种特性使得闭包函数在某些情况下非常有用,例如计数器或迭代器等场景
*/
func closure() func() int {
i := 0
return func() int {
i++
return i
}
}
方法
-
方法和函数类似,但是方法必须要有一个 接收者。这个接收者是一个类型,这样这个方法和该类型联系起来,成为该类型的方法。
-
定义
type Age int // type 定义新类型,约等于int,类似重命名
func (age Age) String(){}
String就是 Age 类型的方法,类型Age是方法的接收者
-
值类型接收者和指针类型接收者
3.1 值类型接收者 就是上面的形式 3.2 指针类型接收者 func (age *Age) Modify(){ *age =Age(30) }
-
所以感觉方法就相当于我们新建的类型的方法,相当于我们新建了一个“类”?
-
在调用方法的时候,传递的接收者本质上都是副本,只不过一个是值的副本,一个是指向这个值的指针的副本
-
其实像上面的指针类型接收者,调用的时候应该用指针调用,我们下面是值调用,也可以使用,是因为go语言自动帮我们转化了
-
Go 语言编译器帮我们自动做的事情:
7.1 如果使用一个值类型变量调用指针类型接收者的方法,Go 语言编译器会自动帮我们取指针调用,以满足指针接收者的要求。 7.2 同样的原理,如果使用一个指针类型变量调用值类型接收者的方法,Go 语言编译器会自动帮我们解引用调用,以满足值类型接收者的要求。
总结: 在 Go 语言中,虽然存在函数和方法两个概念,但是它们基本相同,不同的是所属的对象。 函数属于一个包,方法属于一个类型,所以方法也可以简单地理解为和一个类型关联的函数。 小提示:不管方法是否有参数,通过方法表达式调用,第一个参数必须是接收者,然后才是方法自身的参数。
func main() {
age := Age(25) // 强转为 Age类型
age.String() // 25
(&age).Modify() // 30
age.String()
// 将方法赋值给变量
Aa := Age.String
// 传入一个接收者进行调用
Aa(age)
}
type Age int
func (age Age) String() {
fmt.Println(age)
}
// 指针类型接收者
func (age *Age) Modify() {
*age = Age(30)
}