(Go语言)条件判断与循环?切片和数组的关系?映射表与Map?三组关系傻傻分不清?本文带你了解基本的复杂类型与执行判断语句

1. 条件判断

在Go中,条件控制语句总共有三种ifswitchselect

select相对前两者而言比较特殊

if else

if else 至多两个判断分支,语句格式如下

go 复制代码
if expression {

}

if expression {

}else {

}

expression必须是一个布尔表达式,即结果要么为真要么为假,必须是一个布尔值

同时if语句也可以包含一些简单的语句,例如:

go 复制代码
func main() {
	if x := 1 + 1; x >= 2 {
		fmt.Println(x)
	}
}

if语句相较于其他语言的if语句,基本一致

不同点在于

  • 判断的式子并不需要用括号括起来
  • 可以在判断式子里可以进行赋值声明变量的操作

switch

switch语句也是一种多分支的判断语句,语句格式如下:

go 复制代码
switch expr {
	case case1:
		statement1
	case case2:
		statement2
	default:
		default statement
}

一个简单的例子如下

go 复制代码
func main() {
   str := "a"
   switch str {
   case "a":
      str += "a"
      str += "c"
   case "b":
      str += "bb"
      str += "aaaa"
   default: // 当所有case都不匹配后,就会执行default分支
      str += "CCCC"
   }
   fmt.Println(str)
}

还可以在表达式之前编写一些简单语句,例如声明新变量

go 复制代码
func main() {
	switch num := f(); { // 等价于 switch num := f(); true {
	case num >= 0 && num <= 1:
		num++
	case num > 1:
		num--
		fallthrough
	case num < 0:
		num += num
	}
}

func f() int {
	return 1
}

switch语句也可以没有入口处的表达式。

go 复制代码
func main() {
   num := 2
   switch { // 等价于 switch true { 
   case num >= 0 && num <= 1:
      num++
   case num > 1:
      num--
   case num < 0:
      num *= num
   }
   fmt.Println(num)
}

通过fallthrough关键字来继续执行相邻的下一个分支。

go 复制代码
func main() {
   num := 2
   switch {
   case num >= 0 && num <= 1:
      num++
   case num > 1:
      num--
      fallthrough // 执行完该分支后,会继续执行下一个分支
   case num < 0:
      num += num
   }
   fmt.Println(num)
}

与其他语言的switch不同的是,在Go中,switch循环可以不用判断句子,直接进入判断

label

标签语句,给一个代码块打上标签,可以是gotobreakcontinue的目标。例子如下:

go 复制代码
func main() {
	A: 
		a := 1
	B:
		b := 2
}

单纯的使用标签是没有任何意义的,需要结合其他关键字来进行使用。

goto

goto将控制权传递给在同一函数对应标签的语句,示例如下:

go 复制代码
func main() {
   a := 1
   if a == 1 {
      goto A
   } else {
      fmt.Println("b")
   }
A:
   fmt.Println("a")
}

在实际应用中goto用的很少,跳来跳去的很降低代码可读性,性能消耗也是一个问题。

2. 循环

在Go中,有且仅有一种循环语句 for

Go抛弃了while循环,因为for可以被当作while循环来使用

2.1 for

go 复制代码
for init statement; expression; post statement {
	execute statement
}

当只保留循环条件时,就变成了while

go 复制代码
for expression {
	execute statement
}

示例

这是一段输出[0,20]区间数字的代码

go 复制代码
for i := 0; i <= 20; i++ {
    fmt.Println(i)
}

你可以同时初始化多个变量,然后将其递增

go 复制代码
for i, j := 1, 2; i < 100 && j < 1000; i, j = i+1, j+1 {
	fmt.Println(i, j)
}

双循环打印九九乘法表,这是一个很经典的循环案例

go 复制代码
for i := 1; i < 10; i++ {
    for j := 1; j < 10; j++ {
        if i <= j {
            fmt.Printf("%d * %d = %d \t", i, j, i*j)
        }
    }
    fmt.Println()
}

输出如下

go 复制代码
1*1 =  1  1*2 =  2  1*3 =  3  1*4 =  4  1*5 =  5  1*6 =  6  1*7 =  7  1*8 =  8  1*9 =  9 
2*2 =  4  2*3 =  6  2*4 =  8  2*5 = 10  2*6 = 12  2*7 = 14  2*8 = 16  2*9 = 18
3*3 =  9  3*4 = 12  3*5 = 15  3*6 = 18  3*7 = 21  3*8 = 24  3*9 = 27
4*4 = 16  4*5 = 20  4*6 = 24  4*7 = 28  4*8 = 32  4*9 = 36
5*5 = 25  5*6 = 30  5*7 = 35  5*8 = 40  5*9 = 45
6*6 = 36  6*7 = 42  6*8 = 48  6*9 = 54
7*7 = 49  7*8 = 56  7*9 = 63
8*8 = 64  8*9 = 72
9*9 = 81

2.2 for 作 while 循环使用

go 复制代码
func main() {
	i := 0
	for true {
		if i == 5000 {
			fmt.Println("i 已经突破5000!退出循环")
			return
		}

		if i == 1500 {
			fmt.Println("=---=====================", i)
            i++
			continue
		} else {
			i++
			fmt.Println(i)
		}
	}
}

如果没有其他因素,可以直接放 布尔类型 做表达式,充当while循环

2.3 for range

go 复制代码
func main() {
	str := "hello world"

	/*
		相当于foreach
		i:代表索引值
		v:代表当前索引下在字符串中的值
	*/
	for i, v := range str {
		fmt.Println(i, string(v))
	}
}

输出:

bash 复制代码
0 h
1 e
2 l
3 l
4 o
5  
6 w
7 o
8 r
9 l
10 d

2.4 contine和break配合标签实现逻辑跳转

go 复制代码
/*
在Go中,break、continue这种关键字,都可以配合标签来做到更方便的循环
*/
func main() {
Outer:
    for i := 0; i < 10; i++ {
       for j := 0; j < 10; j++ {
          if i <= j {
             break Outer
          }
          fmt.Println(i, j)
       }
    }
    
Out:
    for i := 0; i < 10; i++ {
       for j := 0; j < 10; j++ {
          if i > j {
             continue Out
          }
          fmt.Println(i, j)
       }
    }
}

是不是有点简单?下面有请 切片 登场!(股掌声!)

3. 认知切片与数组

在Go中,数组和切片两者看起来长得几乎一模一样,但功能有着不小的区别,数组是定长的数据结构,长度被指定后就不能被改变,而切片是不定长的,切片在容量不够时会自行扩容

4. 数组

如果事先就知道了要存放数据的长度,且后续使用中不会有扩容的需求,就可以考虑使用数组,Go中的数组是值类型,而非引用,并不是指向头部元素的指针

数组作为值类型,将数组作为参数传递给函数时,由于Go函数是传值传递,所以会将整个数组拷贝

4.1 如何声明一个数组?

go 复制代码
var a [5]int
var b = [5]int{1, 2, 3, 4, 5}
c := [5]int{1, 2, 3, 4, 5}
// 通过 new函数 获得一个指针
d := new([5]int)

以上几种方式都会给nums分配一片固定大小的内存,区别只是最后一种得到的值是指针。

在数组初始化时,需要注意的是,长度必须为一个常量表达式,否则将无法通过编译,常量表达式即表达式的最终结果是一个常量

go 复制代码
l := 5
var nums [l]int // 错误!!长度必须是常量

4.2 获取

使用数组的下标就可以访问数组中对应的元素,同样的,也可以修改数组中的元素

go 复制代码
var b = [5]int{1, 2, 3, 4, 5}
fmt.Println(b[2]) // 3

b[2] = 12138
fmt.Println(b[2]) // 12138

fmt.Println(b) // [1 2 12138 4 5]

fmt.Println(len(b), cap(b)) // 5 5
  • len函数:返回数组的元素数量
  • cap函数:返回数组的容量,相当于数组长度。容量对于切片才有意义

4.3 切割

go 复制代码
nums := [5]int{1, 2, 3, 4, 5}

var arr []int

arr = nums[1:]  // 子数组范围[1,5) -> [2 3 4 5]
arr = nums[:5]  // 子数组范围[0,5) -> [1 2 3 4 5]
arr = nums[2:3] // 子数组范围[2,3) -> [3]
arr = nums[1:3] // 子数组范围[1,3) -> [2 3]
fmt.Println(arr)

切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开

根据下标来对数组中的值进行切割。

  • 数组在切割后,会变成切片类型

若要将数组转换为切片类型,不带参数进行切片即可,转换后的切片与原数组指向的是同一片内存,修改切片会导致原数组内容的变化

go 复制代码
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:] // 数组转切片类型
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)

// 如果需要对切片进行修改,建议使用clone()函数复制一份出来
arr2 := [5]int{1, 2, 3, 4, 5}
slice2 := slices.Clone(arr2[:])
fmt.Println(slice2)

5. 切片

切片在Go中的应用范围要比数组广泛的多,它用于存放不知道长度的数据,且后续使用过程中可能会频繁的插入和删除元素

5.1 初始化

go 复制代码
var nums []int // 值
nums := []int{1, 2, 3} // 值

nums := make([]int, 0, 0) // 值
nums := new([]int) // 指针

我们能够看到,切片的初始化可以说跟数组没什么区别,仅仅抛弃了一个初始化长度。

在使用切片时,更加推荐使用 make() 创建一个空切片。

  • make函数接收三个参数:类型[]int,长度len(),容量cap()

切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素

切片的底层实现依旧是数组,是引用类型,可以简单理解为是指向底层数组的指针

  • 为什么会更推荐 make() 函数创建切片呢?

通过var nums []int这种方式声明的切片,默认值为nil,所以不会为其分配内存,而在使用make进行初始化时,建议预分配一个足够的容量,可以有效减少后续扩容的内存消耗。

其实和Go开发的中心思想一致,做到创建足够用的变量。

遵守 用多少 就创建多少的原则,例如:能用int8类型的数组就用int8,尽量收缩内存空余

5.2 使用

go 复制代码
nums := make([]int, 0, 0)
fmt.Println(len(nums), cap(nums)) // 0 0

nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 自动扩张;容量始终是要比长度大的

arr := []int{1, 2, 3}
arr = append(arr, 12, 23)
fmt.Println(arr, len(arr), cap(arr))

切片的基本使用与数组完全一致,区别只是切片可以动态变化长度

切片可以通过append函数实现许多操作,函数签名如下,slice是要添加元素的目标切片,elems是待添加的元素,返回值是添加后的切片。

5.3 插入元素

切片元素的插入也是需要结合append函数来使用

go 复制代码
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// 从头部开始插入:插入的数据在前,数组在后
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

// 从指定索引处插入
i := 3
nums = append(nums[:i+1], append([]int{123232}, nums[i:]...)...)
fmt.Println(nums) // i=3,[1 2 3 4 999 999 5 6 7 8 9 10]

// 默认从尾部插入
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

以上例子的插入已经是最简化

5.4 删除元素

切片元素的删除需要结合append函数来使用

  • 为什么要配合append函数?

    append() 方法,其实底子里是一种赋值操作,添加也是直接进行了替换操作

    如果没有设置开始索引,那么它默认是从最后插入,

go 复制代码
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
nums = append(nums, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

n := 3

// 从头部开始删除
nums = nums[n:]   // 删除开头的n个元素
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]

// 从中间指定下标i位置开始删除n个元素
i := 1
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums) // i=2,n=3,[4 8 9 10 11 12 13 14 15 16 17 18 19 20] 删除了 5、6、7

// 从尾部开始删除 n个元素
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [4 8 9 10 11 12 13 14 15 16 17]

// 删除所有元素
nums = nums[:0]
fmt.Println(nums) // []

5.5 拷贝

切片在拷贝时需要确保目标切片有足够的长度

go 复制代码
dest := make([]int, 0)
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(arr, dest)
// copy():第一个参数是用于存放接收的数组;第二个参数需要一个被复制的数组
fmt.Println(copy(dest, arr))
fmt.Println(arr, dest)

5.6 遍历

切片遍历与数组完全一致,就不细讲了

go 复制代码
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for i := 0; i < len(slice); i++ {
    fmt.Println(slice[i])
}

slice = []int{1, 2, 3, 4}
for index, val := range slice {
    fmt.Println(index, val)
}

5.7 多维切片

go 复制代码
var nums [5][5]int
fmt.Println(nums) // [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

nums2 := make([][]int, 5)
fmt.Println(nums2) // [[] [] [] [] []]

make()函数在创建二维数组的时候,并不会为切片中添加默认值

所以,在以make()函数创建多维数组的时候,还需要单独初始化

  • 因为make函数只会帮你初始化最开始的那一层,二维之后的内容需要自己单独初始化
go 复制代码
//nums2[2][2] = 12 // 在未初始化前,不可以对数组做任何操作
for i := range nums2 {
    nums2[i] = make([]int, len(nums))// 初始化二维内容
}
nums2[2][2] = 12

fmt.Println(nums2) // [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

tips:在数组未初始化前,我们无法对数组做任何与值有关的操作

报错:

bash 复制代码
index out of range [2] with length 0

goroutine 1 [running]:
main.main()
	E:/Golang/学习案例/1_base/8_slice/demo9.go:11 +0x2e5

5.8 拓展(切片)表达式

只有切片才能使用拓展表达式

切片与数组都可以使用简单表达式来进行切割,但是拓展表达式只有切片能够使用,该特性于Go1.2版本添加,主要是为了解决切片共享底层数组的读写问题,主要格式为如下,需要满足关系low<= high <= max <= cap,使用拓展表达式切割的切片容量为max-low

go 复制代码
slice[low:high:max]

那么这么做就会有一个明显的问题,s1s2是共享的同一个底层数组,在对s2进行读写时,有可能会影响的s1的数据

go 复制代码
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
// 添加新元素,由于容量为6.所以没有扩容,直接修改底层数组(把s1也顺道改了,这合理吗?)
s2 = append(s2, 1)

fmt.Println(s1) // [1 2 3 4 1 6 7 8 9]
fmt.Println(s2, len(s2), cap(s2)) // [4 1] 2 6

slice表达式分为简单表达式slice[low,high]和扩展表达式slice[low : high : max];

简单表达式作用于数组、切片时产生新的切片,作用于字符串时产生新的字符串;

扩展表达式只能作用于数组、切片,不能作用于字符串。

tips:

新切片b ( b := a[low, high])不仅可以读写a[low]至a[high-1]之间的所有元素,而且在使用append(b, x)函数增加新的元素x时,还可能会覆盖a[high]及后面的元素

5.9 clear 清除

在go1.21新增了clear内置函数,clear会将切片内所有的值置为零值,

go 复制代码
package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s) // [0 0 0 0]
}

如果想要清空切片,可以用拓展表达式

go 复制代码
func main() {
	s := []int{1, 2, 3, 4}
    s = s[:0:0]
	fmt.Println(s)
}

限制了切割后的容量,这样可以避免覆盖原切片的后续元素。

6. 映射表

一般来说,映射表数据结构实现通常有两种,哈希表(hash table)和搜索树(search tree),区别在于前者无序,后者有序。在Go中,map的实现是基于哈希桶(也是一种哈希表),所以也是无序的。

6.1 初始化

在Go中,map的键类型必须是可比较的,比如string int是可比较的,而[]int是不可比较的,也就无法作为map的键。

6.1.1 字面量初始化

go 复制代码
map[keyType]valueType{}
go 复制代码
nameMap := map[int]string{
    1: "张三",
    2: "李四",
}
//fmt.Println(nameMap[1])
for i := range nameMap {
    /*
            张三
            李四
        */
    fmt.Println(nameMap[i])
}

6.1.2 make 函数初始化

使用内置函数make,对于map而言,接收两个参数,分别是类型与初始容量

go 复制代码
ageMap := make(map[string]int, 5)
ageMap["张三"] = 12
ageMap["李四"] = 16
for s := range ageMap {
    fmt.Println(ageMap[s])
}

6.1.3 tips

map是引用类型,零值或未初始化的map可以访问,但是无法存放元素,所以必须要为其分配内存

go 复制代码
func main() {
   var mp map[string]int
   mp["a"] = 1
   fmt.Println(mp) // panic: assignment to entry in nil map
}

在初始化map时应当尽量分配一个合理的容量,以减少扩容次数。

还是Go的一种规范,尽力减少内存消耗

6.2 访问

访问一个map的方式就像通过索引访问一个数组一样

go 复制代码
mp := map[string]int{
    "a": 0,
    "b": 1,
    "c": 2,
    "d": 3,
}
fmt.Println(mp["a"]) // 0
fmt.Println(mp["b"]) // 1
fmt.Println(mp["d"]) // 3
fmt.Println(mp["f"]) // 0

通过代码可以观察到,即使map中不存在"f"这一键值对,但依旧有返回值。

map对于不存的键其返回值是对应类型的零值,并且在访问map的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在

go 复制代码
// 访问map,第一个返回值,第二个返回是否存在
if val, exist := mp["f"]; exist {
    fmt.Println(val)
} else {
    fmt.Println("key不存在")
}

求长度,还是使用len()方法。

其实把这个当作js中的length属性就可以了

go 复制代码
len(mp)

6.3 存值

map的存值与数组存值类似

存储已经存在的键时,会覆盖掉原有的值

go 复制代码
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
if _, exist := mp["b"]; exist {
    mp["b"] = 3
}
fmt.Println(mp)// map[a:1 b:3]

不过,也有个特殊情况:

go 复制代码
func main() {
	mp := make(map[float64]string, 10)
	mp[math.NaN()] = "a"
	mp[math.NaN()] = "b"
	mp[math.NaN()] = "c"
	_, exist := mp[math.NaN()]
	fmt.Println(exist)// false
	fmt.Println(mp) // map[NaN:a NaN:b NaN:c]
}

通过结果可以观察到相同的键值并没有覆盖,反而还可以存在多个,也无法判断其是否存在,也就无法正常取值。

因为NaN是IEE754标准所定义的,其实现是由底层的汇编指令UCOMISD完成,这是一个无序比较双精度浮点数的指令,该指令会考虑到NaN的情况,因此结果就是任何数字都不等于NaN,NaN也不等于自身,这也造成了每次哈希值都不相同。关于这一点社区也曾激烈讨论过,但是官方认为没有必要去修改

总归一句话,尽量避免使用NaN作为map的键。

6.4 删除

go 复制代码
func delete(m map[Type]Type1, key Type)

删除一键值对需要用到内置函数delete,例如

go 复制代码
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(mp)// map[a:0 b:1 c:2 d:3]
   delete(mp, "a")
   fmt.Println(mp)// map[b:1 c:2 d:3]
}

需要注意的是,如果值为NaN,甚至没法删除该键值对。

go 复制代码
func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   fmt.Println(mp) // map[NaN:c NaN:a NaN:b]
   delete(mp, math.NaN())
   fmt.Println(mp) // map[NaN:c NaN:a NaN:b]
}

6.5 删除

在go1.21之前,想要清空map,就只能对每一个map的key进行delete

但是go1.21更新了clear函数,就不用再进行之前的操作了,只需要一个clear就可以清空

go 复制代码
m := map[string]int{
    "a": 1,
    "b": 2,
}
// 1.21 以前必须一个个删除
for k, _ := range m {
    delete(m, k)
}

m["a"] = 1
m["b"] = 2
// 1.21 更新clear函数,直接清空
clear(m)
fmt.Println(m)

6.6 注意

map并不是一个并发安全的数据结构,Go团队认为大多数情况下map的使用并不涉及高并发的场景,引入互斥锁会极大的降低性能

map内部有读写检测机制,如果冲突会触发fatal error。例如下列情况有非常大的可能性会触发fatal

go 复制代码
func main() {
   group.Add(10)
   // map
   mp := make(map[string]int, 10)
   for i := 0; i < 10; i++ {
      go func() {
         // 写操作
         for i := 0; i < 100; i++ {
            mp["helloworld"] = 1
         }
         // 读操作
         for i := 0; i < 10; i++ {
            fmt.Println(mp["helloworld"]) // fatal error: concurrent map writes
         }
         group.Done()
      }()
   }
   group.Wait()
}

在这种情况下,需要使用sync.Map来替代。

7. 😍前篇知识回顾

  1. Go的环境安装与开发工具配置
  2. Go的运行流程步骤与包的概念
  3. (Go)变量与常量?字面量与变量的较量!
  4. 初上手Go?本篇文章帮拿捏Go的数据类型!

8. 💕👉 其他好文推荐

全文资料学习全部参考于:Golang中文学习文档

相关推荐
MClink5 小时前
Go怎么做性能优化工具篇之pprof
开发语言·性能优化·golang
m0_748254667 小时前
go官方日志库带色彩格式化
android·开发语言·golang
Algorithm157610 小时前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
Narutolxy11 小时前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader12 小时前
全面解析 Golang Gin 框架
开发语言·golang·gin
hkNaruto1 天前
【P2P】【Go】采用go语言实现udp hole punching 打洞 传输速度测试 ping测试
golang·udp·p2p
入 梦皆星河1 天前
go中常用的处理json的库
golang
海绵波波1071 天前
Gin-vue-admin(2):项目初始化
vue.js·golang·gin
每天写点bug1 天前
【go每日一题】:并发任务调度器
开发语言·后端·golang
一个不秃头的 程序员1 天前
代码加入SFTP Go ---(小白篇5)
开发语言·后端·golang