go语言基础(二):匿名函数、数组、随机数、切片、字符串
1. 匿名函数
匿名函数的使用常用的有两种情况
-
定义并调用匿名函数
func main() { // 匿名函数的定义 // func后面的第一个括号代表函数的形参列表 // func后面的第一个括号后可以跟返回值 // 在func后面的反括号后还要再加上一个括号,代表函数的调用,里面要写函数的实参 func() { fmt.Println("hello world") }() func(a int, b int) { fmt.Println(a+b) }(10, 20) }
-
定义匿名函数,将该函数赋值给函数变量
type FUNCTYPE func(int) func main() { var f FUNCTYPE // 可以使用 := 进行自动类型推导,那么久不用定义函数变量 f = func(int){ fmt.Println("this is func(int)") } f(10) }
2. 数组
- 数组的定义:
var 数组名 [数组元素个数]数据类型
- 数组定义时的初始化:
var 数组名 [数组元素个数]数据类型 = [数组元素个数]数据类型{数值,数值,...}
- 可以使用自动推导直接定义出一个数组
其他的数组特性就不用过多赘述了,和C语言的相同
func main() {
// 数组的定义
// var 数组名 [数组元素个数]数据类型
var arr [10]int = [10]int{1, 2, 3, 4}
// 默认数组的初始值为0
fmt.Println(arr)
// 使用自动推导
arr1 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Printf("%T\n", arr1)
fmt.Println(arr1)
fmt.Println(len(arr1)) // 计算数组元素个数
// 遍历数组
for i:=0; i<len(arr1); i++ {
fmt.Printf("%d ", arr1[i])
}
fmt.Println()
// 范围遍历数组
for i, v := range arr1 {
fmt.Println("index =", i, "value =", v)
}
// 这种初始化方式根据后面的初始化的元素个数决定数组大小
arr2 := [...]int{1, 2, 3, 4, 5}
fmt.Println(arr2)
}
// 结果
[1 2 3 4 0 0 0 0 0 0]
[10]int
[1 2 3 4 5 6 7 8 9 10]
10
1 2 3 4 5 6 7 8 9 10
index = 0 value = 1
index = 1 value = 2
index = 2 value = 3
index = 3 value = 4
index = 4 value = 5
index = 5 value = 6
index = 6 value = 7
index = 7 value = 8
index = 8 value = 9
index = 9 value = 10
范围遍历的语法:(可以使用匿名变量做占位_
)
for 下标, 值 := range 数组名 {
语句
}
数组的存储
-
与C语言区分开,数组的地址要使用
&数组名
来访问,C语言是数组名直接就是数组的首地址,没有这个&
-
数组的地址和数组中的元素的首地址是相同的,这一点和C语言是相同的
func main() {
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 打印数组的元素地址
fmt.Printf("%p\n", &arr)
// 打印数组中的元素的地址
for i, _ := range arr {
fmt.Printf("%p\n", &arr[i])
}
}
// 结果
0xc420080000
0xc420080000
0xc420080008
0xc420080010
0xc420080018
0xc420080020
0xc420080028
0xc420080030
0xc420080038
0xc420080040
0xc420080048
以上代码中打印出来了数组的地址和数组中的每个元素的地址,由此可见,数组的地址和数组中首元素的地址是相同的,而且在go语言中64位机上int是占8个字节,所以每个数组元素的地址差8。(32位机上int占4字节)
数组的初始化
可以使用一个数组给另外一个数组进行初始化赋值
func main() {
arr := [5]int{1, 2, 3, 4, 5}
var arr1 [5]int
arr1 = arr
fmt.Println(arr)
fmt.Println(arr1)
}
此时这两个数组是独立的两个数组。
注意:在进行数组的如上赋值操作时必须有相同的元素个数和相同的数据类型
- 如果想要将两个不同类型的数组进行赋值,首先不能使用数组的类型转换,这样是不支持的,可以使用for循环进行遍历赋值
数组的逆置练习
在C语言中的想法是使用一个while循环,双指针来进行交换,而go语言中首先没有while,那么在go语言中它的for就可以达成while的操作。
for 条件表达式 {
语句
}
func main() {
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(arr)
i := 0
j := len(arr)-1
// 这个for就是类似于C语言的while
for i < j {
arr[i], arr[j] = arr[j], arr[i]
i++
j--
}
fmt.Println(arr)
}
3. 随机数
和C语言一样,如果需要定义真正的随机数,需要设置随机数种子,需要导入两个包
import (
"math/rand"
"time"
)
rand.Seed(time.Now().UnixNano())
生成随机数
rand.Intn(生成的数字模除的值)
func main() {
// 设置随机数种子
rand.Seed(time.Now().UnixNano())
// 生成一个随机数
// 生成随机数的函数的参数表示
num := rand.Intn(100)
fmt.Println(num)
for i:=0; i<10; i++ {
fmt.Println(rand.Intn(100))
}
}
4. 二维数组
语法:var 数组名 [行的个数][列的个数]数据类型
func main() {
// var arr [行的个数][列的个数]数据类型
var arr [2][3] int
// 数组的初始化赋值
// 二维数组在赋值的时候必须要写每一个行的大括号,如果不写是报错的
arr = [2][3]int{
{1, 2, 3},
{4, 5, 6}, // 如果这样换行写的话,这里必须要写一个逗号才能将下面的大括号换行写,比较好看
}
// len(二维数组) 表示二维数组的行的个数
fmt.Println(len(arr))
fmt.Println(len(arr[0]))
// 遍历二维数组
for i:=0; i<len(arr); i++ {
for j:=0; j<len(arr[0]); j++ {
fmt.Print(arr[i][j], " ")
}
fmt.Println()
}
}
5. 切片
-
数组中的元素个数在定义的时候必须是一个常量或者是一个常量表达式
var count int = 10 var arr [count]int
以上代码在语法检查不会报错,在编译时报错,显示变量无法用来定义元素个数
-
数组无法进行元素的扩展
通过切片可以解决以上问题
5.1 切片的定义及添加数据
- 定义一个空切片:
var 切片名 []数据类型
- 定义出来的是一个空切片,不能直接用来进行下标访问,需要添加数据后才能使用
- 为切片添加数据:
切片名 = append(切片名, 元素1, 元素2, ...)
func main() {
// 创建一个空切片
var slice []int
// 为切片添加数据
slice = append(slice, 123)
slice = append(slice, 456, 789)
fmt.Println(slice)
}
- 定义切片并初始化切片的大小
- 语法1:
var 切片名 []数据类型 = make([]数据类型, 元素个数)
,此时元素个数与容量相同 - 语法2:
var 切片名 []数据类型 = make([]数据类型, 元素个数, 容量大小)
,长度要小于等于容量
- 语法1:
func main() {
// 在创建切片的时候直接指定切片的大小
// 语法:var 切片名 []数据类型 = make([]数据类型, 元素个数)
// 此时会将切片初始化元素个数为指定个数并初始化数据为0
var slice []int = make([]int, 10)
slice[0] = 123
slice[1] = 234
fmt.Println(slice)
// 打印切片元素个数
fmt.Println(len(slice))
// 打印切片容量
fmt.Println(cap(slice))
slice = append(slice, 123, 456)
// 打印切片元素个数
fmt.Println(len(slice))
// 打印切片容量
// 容量扩充为上一次的两倍
fmt.Println(cap(slice))
}
// 结果
[123 234 0 0 0 0 0 0 0 0]
10
20
12
20
- 注意:切片的扩容步骤:寻找足够的空间,将原数据拷贝到新的空间,释放原空间,随后再添加数据。因为扩容的步骤比较繁琐且有很多的时间开销,所以每次在容量满时不会仅仅扩容一个元素的大小,而是会预留出大小。
- 使用自动推导创建切片
slice := []int{1, 2, 3, 4, 5}
fmt.Println(len(slice))
fmt.Println(cap(slice))
// 以上两个结果都是5
slice := make([]int, 5)
fmt.Println(len(slice))
fmt.Println(cap(slice))
// 以上两个结果都是5
注意:不要使用cap进行打印操作
5.2 切片的打印
使用下标或者for-range即可
for i, v := range slice {
fmt.Println("index =", i, "value =", v)
}
5.3 切片的拷贝
- 提到切片的拷贝首先要说切片的实际内容
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice)
fmt.Println(unsafe.Sizeof(slice)) // 输出24
}
slice的大小是24,原因是切片实际内容并不是其中的数据,而是它维护了三个变量,分别是切片的大小、切片的容量和存储数据的地址,真正的数据是存储在这个指向的地址中的。
- 那么所以在发生拷贝时,进行的浅拷贝,也就是说拷贝前后两个变量指向的是同一个切片,修改了其中的一个切片,另一个切片的值也会一并更改
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice)
fmt.Println(unsafe.Sizeof(slice)) // 输出24
si := slice
fmt.Println(si) // [1 2 3 4 5]
slice[0] = 123
fmt.Println(si) // [123 2 3 4 5]
}
-
copy()函数会使发生深拷贝,此时更改一个的值不会更改另一个。
copy()函数:参1 dest,参2 source
// 深拷贝 sli := make([]int, 5) copy(sli, slice) fmt.Println(sli) slice[0] = 234 fmt.Println(sli) fmt.Println(slice)
5.4 切片的截取
切片的截取都是在原切片的基础上进行操作的,所以切片截取后的地址和原切片相同,修改截取前的原切片也会对截取后的结果造成影响。
- 切片截取的语法:
目标值 := 原切片[开始下标:结束下标]
,截取的内容不包括结束下标元素
func main() {
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := slice[0:5]
fmt.Println(s1)
fmt.Printf("%p\n", slice)
fmt.Printf("%p\n", s1)
}
// 结果
[1 2 3 4 5]
0xc4200160a0
0xc4200160a0
- 切片的截取的写法
slice[起始位置:] // 从起始位置到切片的结尾
slice[:] // 切片的全部
slice[:结束位置] // 从0开始到结束位置,不包括结束位置
-
切片截取后的容量
- 切片的截取公式:
slice[low:high:max]
- 容量:
max-low
- 长度:
high-low
- 当max没有指定的时候,默认是切片的容量
- 最大容量要大于结束位置
s2 := slice[2:5:8] fmt.Println(s2) // [3, 4, 5] fmt.Println(len(s2)) // 3 fmt.Println(cap(s2)) // 6
- 切片的截取公式:
5.5 切片做函数参数
func test1(slice []int) {
fmt.Println(slice)
}
func test2(slice ...int){ // 可以使用不定参进行传递切片,因为不定参的原型就是切片
fmt.Println(slice)
}
func main() {
slice := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
test1(slice)
test2(slice...) // slice...的原型是切片
}
尽量不要写不定参,不易读
- 切片做函数参数传参发生的是值传递,传递的是实际存储数据的地址,所以传进函数和外面的是同一个切片,形参可以修改形参的值
5.6 切片的扩容
- 一般情况,当切片元素个数要超过切片容量时,每次扩容都是上一次的倍数(2倍)
- 特殊情况
- 当一次性append多个元素导致切片元素大于容量时,切片容量是添加元素后的切片总元素个数的向上取偶
- 当容量超过1024时,扩容至原先的1.25倍
5.7 切片的append函数参数传递
若想使一个切片添加到另一个切片的后面,可以使用append函数,但是参数的传递要注意,不能直接使用切片名,要使用不定参传递,因为append函数的第二个参数需要的是元素而不是一个切片。
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{6, 7 ,8, 9, 10}
for _, v := range slice2 {
slice1 = append(slice1, v)
}
fmt.Println(slice1)
// 使用append函数
slice1 = append(slice1, slice2...) // 要使用不定参的方式传递第二个参数
fmt.Println(slice1)
}
5.8 字符串与字符切片相互转换
使用[]byte()
对字符串进行强制转换,可以得到一个字符切片,打印这个切片时默认元素是字符的ASCII码值
使用string()
可以对字符切片强制转换为字符串
func main() {
str := "hello"
slice := []byte(str)
fmt.Println(slice)
for _, v := range slice {
fmt.Printf("%c", v)
}
fmt.Println()
str2 := string(slice)
fmt.Println(str2)
}
// 结果
[104 101 108 108 111]
hello
hello
6. 字符串
- 字符串在末尾有一个
\0
作为结束标记,算在字符串的实际长度中 - 使用
len()
对字符串求长度,计算的是\0
之前的长度
6.1 字符串与汉字
- 汉字占3个字节,汉字是使用ASCII码的128-256直接实现的
func main() {
str := "我爱学习"
for i:=0; i<len(str); i++ {
fmt.Println(str[i])
}
}
// 结果
230
136
145
231
136
177
229
173
166
228
185
160
以上结果,每三个数字是一个str中的汉字
6.2 字符串的操作函数
-
字符串包含
str1 := "我爱学习" str2 := "学习" // strings.Contains(str, substr) 返回str1中是否包含str2 // 用作于模糊查找 fmt.Println(strings.Contains(str1, str2))
-
字符串拼接
slice := []string{"我", "爱", "学", "习"} // 字符串拼接,将一个字符串切片中的数据使用参数2的字符串作为连接符拼接成一个字符串 str := strings.Join(slice, "") fmt.Println(str)
-
字符串查找
str1 := "hello" str2 := "llo" // strings.Index() 用于找到参数2在参数1中出现的位置,如果不存在则返回-1 num := strings.Index(str1, str2) fmt.Println(num) str3 := "abc" fmt.Println(strings.Index(str1, str3))
-
字符串重复
str1 := "我爱学习" // 将字符串重复n次 ch := strings.Repeat(str1, 3) fmt.Println(ch)
-
字符串替换
str := "hello" // 字符串替换函数,参1是要操作的字符串,参2是要被替换的字符,参3是替换成为的字符,参4是替换的次数 // 默认从左向右进行搜素 str = strings.Replace(str, "l", "s", 2) fmt.Println(str)
-
字符串收尾去除指定字符
str := " ab c, cde " // strings.Trim() 用于去掉参1字符串的头尾的参2字符串中的字符,这个字符可以是多个,字符串中间的该字符不会被去掉 ch := strings.Trim(str, " ") fmt.Println(ch) str2 := "======a=====aaaaaah=ello=====aaaaaa====" ch2 := strings.Trim(str2, "a=") fmt.Println(ch2)
-
字符串去空格
str := "are u ok" // strings.Fields() 用于去掉字符串中的空格,并存在一个切片中 slice := strings.Fields(str) for _, v := range slice { fmt.Println(v) }
-
字符串分割
str := "123-456-789" // strings.Split() 用于将参1的字符串通过参2进行分割,分割的结果是一个字符串切片 ch := strings.Split(str, "-") fmt.Println(ch)
6.3 字符串类型转换
-
Format系列函数将其他类型数据转换为字符串类型
func main() { // 从其他类型转换为字符串类型 // 布尔类型转为字符串 b := true str := strconv.FormatBool(b) fmt.Println(str) fmt.Printf("%q\n", str) // %q 的占位符打印带双引号 // 整型转换为字符串 str1 := strconv.FormatInt(10, 2) // 参数1是一个int64类型的数据,转换为参数2的进制后转换为字符串 // PS: 最高36进制,最低2进制 fmt.Println(str1) str2 := strconv.Itoa(123445) fmt.Println(str2) // 将浮点型转换为字符串 // 参1:要进行转换的数据,参2:表示数据的类型是一个浮点型,参3:表示处理过后小数点后面留几位,参4:处理的方式(64位和32位两种,其他报错) // 其中参3的小数保留位数会四舍五入 str3 := strconv.FormatFloat(3.14, 'f', 5, 64) fmt.Println(str3) }
-
Parse系列函数将字符串类型转换为其他类型
func main() { // 将字符串类型转换为其他类型 // 字符串转布尔类型 // strconv.ParseBool函数有两个返回值,一个是返回的对应类型的值,一个是err错误值 // err默认是nil, 当err不是nil的时候,说明出错了,另一个返回值也就不用接收了 bl, err := strconv.ParseBool("true") if err != nil { fmt.Println(err) } fmt.Println(bl) fmt.Printf("%T\n", bl) // 字符串转整型 // strconv.ParseInt 参数1:待转换的值,参数2:把参数1当成什么进制来进行转换,参数3:指定结果必须能无溢出赋值的整数类型 // 返回值是int64类型的 // 注意:不能像C语言一样如果出现字母解析字母前的数字,在这里会直接设置err,返回值是0 v, _ := strconv.ParseInt("123", 10, 64) fmt.Println(v) fmt.Println(unsafe.Sizeof(v)) // 字符串转整型 i, _ := strconv.Atoi("123456") fmt.Println(i) // 字符串转浮点型 f, _ := strconv.ParseFloat("123.456", 64) fmt.Println(f) }
6.4 其他类型转字符切片
使用append系列函数
func main() {
// 其他类型转换为字符切片
var slice []byte
// append系列函数会向切片中进行追加,不会覆盖
slice = strconv.AppendBool(slice, true)
slice = strconv.AppendInt(slice, 123, 10)
slice = strconv.AppendFloat(slice, 3.14, 'f', 6, 64)
// 这里的转换字符切片比直接类型转换多一对""
slice = strconv.AppendQuote(slice, "hello")
fmt.Println(string(slice))
}