(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中文学习文档

相关推荐
Bony-3 小时前
Go语言垃圾回收机制详解与图解
开发语言·后端·golang
吴老弟i15 小时前
Go 多版本管理实战指南
golang·go
Grassto18 小时前
HTTP请求超时?大数据量下的网关超时问题处理方案,流式处理,附go语言实现
后端·http·golang·go
Paul_092018 小时前
golang编程题2
开发语言·后端·golang
代码N年归来仍是新手村成员18 小时前
【Go】从defer关键字到锁
开发语言·后端·golang
源代码•宸1 天前
Leetcode—746. 使用最小花费爬楼梯【简单】
后端·算法·leetcode·职场和发展·golang·记忆化搜索·动规
x70x802 天前
Go中nil的使用
开发语言·后端·golang
源代码•宸2 天前
Leetcode—47. 全排列 II【中等】
经验分享·后端·算法·leetcode·面试·golang·深度优先
漫漫求2 天前
Go的panic、defer、recover的关系
开发语言·后端·golang
Tony Bai2 天前
2025 Go 官方调查解读:91% 满意度背后的隐忧与 AI 时代的“双刃剑”
开发语言·后端·golang