Go 语言基础语法和常用特性解析 | 豆包MarsCode AI刷题

36 阅读26分钟

Go基础语法

记录Go的基础语法,配合使用例子,加深对Go语言基础语法的熟悉,并作为自己的Go基础语法查询手册

变量与常量

  1. 标识符

Go语言的标识符由字母(AZ, az)、数字(0~9)、下划线(_)组成,区分大小写,不能以数字开头,只能以字母和下划线开头,Go语言中使用驼峰命名法命名变量,例如使用ListenAndServeReadFromFile

  1. 数据类型

datatype1.jpg

datatype2.jpg

func main () {
	// 复数型
	var c1 complex128
	c1 = 3 + 4i

	// 构造复数
	c := complex(1.2, 3.4)
	// 获取实部
	fc1 := real(c)
	// 获取虚部
	fc2 := imag(c)

	// 数字字面量
	v1 := 0b00101101 // 二进制101101,相当于十进制45
	v2 := 0o755 // 八进制755,相当于十进制493
	v3 := 0x2d // 十六进制2d,相当于十进制45

	// 定义一个多行字符串
	str := `
		这是一个多行字符串
		这是第二行
		这是第三行
	`
}
  • 字符串修改

字符串的底层实现是一个结构体,包含指向底层字节数组的指针和字节数组的长度

type stringStruct struct {
	str unsafe.Pointer
	len int
}

要修改字符串,需要先将其转换成[]rune[]byte,然后转换成string,无论哪种转换,都会重新分配内存,并复制字节数组

func main () {
	s1 := "big"
	byteS1 := []byte(s1)
	byteS1[0] = 'p'

	s2 := "白萝卜"
	runeS2 := []rune(s2)
	runeS2[0] = '红'
}

组成每个字符串的元素是字符,用单引号包裹,Go中字符有两种类型,byte代表ASCII码字符,rune代表UTF-8字符,这两个本质上都是整型,只是为了方便编程,Go语言才给出这两个别名,其中byte是uint8的别名,rune是int32的别名

  • 类型转换

数值类型之间可以互相转换,但字符串类型不能与布尔类型、数值类型相互转换

强制类型转换存在溢出情况,当字节长度大的数值转换为字节长度小的数值时,大数值的高位被截位,导致数据精度丢失;浮点数转换为整数时,小数部分会被截掉;可以使用strconv包进行转换,该包实现了基本数据类型和其字符串表示的相互转换

func main () {
	var a = 1
	c := float32(a)
	// 整数转换成字符串
	i := 123
	// 使用fmt.Sprintf返回一个格式化的字符串
	s1 := fmt.Sprintf("%d", i)
	// 使用strconv.Itoa返回一个字符串
	s2 := strconv.Itoa(i)
	// 使用strconv.FormatInt返回一个字符串
	s3 := strconv.FormatInt(int64(i), 10)
	// 使用strconv.FormatUint返回一个字符串
	s4 := strconv.FormatUint(uint64(i), 10)
	
	// 字符串转换成整数
	s := "123"
	// 使用strconv.Atoi返回一个整数
	i1, _ := strconv.Atoi(s)
	// 使用strconv.ParseInt返回一个整数
	i2, _ := strconv.ParseInt(s, 10, 64)
	// 使用strconv.ParseUint返回一个无符号整数
	i3, _ := strconv.ParseUint(s, 10, 64)

	// 字符串转换成浮点数
	s = "123.456"
	// 使用strconv.ParseFloat返回一个浮点数
	f, _ := strconv.ParseFloat(s, 64)
}
  1. 基本使用
  • 变量声明
func main() {
	// 变量声明
	var name string
	var age int
	var isOk bool

	// 批量声明
	var (
		name1 string
		age1  int
		isOk1 bool
	)
}
  • 变量初始化
func main() {
	// 声明时指定初始值
	var name string = "zhangsan"
	
	// 一次初始化多个变量
	var name, age = "zhangsan", 20

	// 省略类型,编译器自动推导类型
	var name = "zhangsan"
	var age = 20

	// 语法糖,简短声明并初始化方式
	name := "zhangsan"
	age := 20
}
  • 常量

声明后恒定不变的值

func main() {
	// 常量声明
	const pi = 3.1415

	// 一次性声明多个常量
	const (
		pi = 3.1415
		e = 2.71828
	)

	// 可省略值
	const (
		n1 = 100
		n2		// 100
		n3		// 100
	)

	// 使用预声明标识符
	const (
		leve10 = iota	//0
		leve11 = iota	//1
		// 可省略
		leve12			//2
		// 可跳过
		_
		leve13			//4
		// 可插队
		leve14 = 100	//100
		leve15			//6
		// 一行定义多个
		leve16, leve17 = iota, iota	//7, 8
		leve18, leve19	//8, 9

		// 定义数量级
		_ = iota
		KB = 1 << (10 * iota)	//1 << (10 * 1)
		MB = 1 << (10 * iota)	//1 << (10 * 2)
		GB = 1 << (10 * iota)	//1 << (10 * 3)
		TB = 1 << (10 * iota)	//1 << (10 * 4)
	)
}

条件语句

Go中规定与if匹配的左括号必须与if在同一行,与else匹配的左括号必须与else在同一行,同时,else必须与上一个if或else if右边的右括号在同一行

if或else if其后的条件语句不需要括号

特殊用法:可以在if表达式之前添加一个执行语句,再根据变量值进行判断

func main() {
	score := 100	// score作用域是整个函数
	if score >= 90 {
		fmt.Println("优秀")
	} else if score >= 80 {
		fmt.Println("良好")
	} else {
		fmt.Println("及格")
	}

	// 特殊用法
	if score := 90; score >= 90 {	// score作用域是if语句块
		fmt.Println("优秀")
	} else if score >= 80 {
		fmt.Println("良好")
	} else {
		fmt.Println("及格")
	}
}

循环语句

Go中只有for一种循环语句,所有循环类型均可以使用for来实现

for后无需括号

func main() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}

	// 省略初始语句
	i := 10
	for ; i < 20; i++ {
		fmt.Println(i)
	}

	// 省略初始语句和结束语句,实现while循环
	for i < 30 {
		fmt.Println(i)
		i++
	}

	// 使用break、goto、return、panic语句强制退出for循环,或者通过continue跳出本次循环进入下一次循环
	for i := 0; i < 10; i++ {
		if i == 5 {
			break
		}
		if i == 3 {
			continue
		}
		if i == 7 {
			panic("panic")
		}
		if i == 8 {
			goto label
		}
	}
label:
    fmt.Println("hello")

	for {
		fmt.Println("无限循环")
	}
}

switch分支

switch可用于多条件判断,化简条件判断分支过多的情况

Go中的fallthrough语法可以在执行完满足条件的case分支后继续执行下一个分支,且fallthrough语句只能作为case分支的最后一个语句出现

func main() {
	score := 100
	switch score {
	
	// 可使用多值判断
	case 10, 20, 30:
		fmt.Println("10, 20, 30")
	
	// 使用fallthrough
	case 40:
		fmt.Println("40")
		fallthrough		// 此后的语句都会执行,输出50、60、default
	case 50:
		fmt.Println("50")
		fallthrough
	case 60:
		fmt.Println("60")
	default:
		fmt.Println("default")
	}
	
	// 使用表达式做判断
	switch {
		case score == 100:
			fmt.Println("100")
		case score == 200:
			fmt.Println("200")
		default:
			fmt.Println("default")
	}
}

标签

Go中标签被定义后必须被使用,标签名允许与变量名重复,但标签名不能重复

  • goto

goto语句通过标签在代码间无条件跳转,能快速跳出循环、避免重复退出,能简化一些代码的实现过程

func main() {
	// 使用goto跳出双层for循环
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 20 {
				// 跳转到退出标签
				goto breakTag
			}
			fmt.Println(i, j)
		}
	}
	return		// 若不加return,则后面breakTag标签没有goto跳转也会被执行

breakTag:
	fmt.Println("breakTag")
}
  • break

break语句可以结束for、switch和select语句,但不能用于goto语句,可以在break语句后面添加标签,表示退出某个标签对应的代码块,标签名必须定义在对应的for、switch和select语句块之前

func main() {
breakLabel:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				fmt.Println(j)
				break breakLabel // 跳出,不再执行breakLabel下的循环语句,即只会输出2
			}
		}
		fmt.Println(i)
	}
}
  • continue

continue语句可以结束当前循环,开始下一次循环,但是仅限在for循环中使用,在continue语句后添加标签,表示继续标签对应的下一次循环

func main() {
breakLabel:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				fmt.Println(j)
				continue breakLabel //跳出内层循环,继续执行外层循环,即只会打印十个2
			}
		}
		fmt.Println(i)
	}
}

数组

数组的大小从声明时就确定了,使用时可以修改数组成员,但是不能修改数组大小,因此平常不常使用数组,而更多使用切片,其长度可以变化

数组是占用一块连续内存的值类型,赋值操作和函数传参会复制整个数组得到一个副本,修改副本的值不会改变原始变量的值

func testArray(a [2]int) {
	a[0] = 100
	fmt.Println(a)		// [100 2]
}

func main() {
	a := [2]int{1, 2}
	testArray(a)
	fmt.Println(a)		// [1 2]
}
  • 数组的声明

[3]int[4]int是不同类型,不能直接进行比较和赋值

func main() {
	// 声明一个数组
	var a [3]int
	var b [4]int

	var n = 5
	// 数组的长度必须是常量或常量表达式
	// var c [n]int		// error

	// 可以通过索引访问数组成员
	fmt.Println(a[1], b[2])

	// 使用内置len函数获取数组中元素数量
	fmt.Println(len(a), len(b))
}
  • 数组的初始化
func main() {
	var a [3]int					// 初始化为int类型的零值
	
	var b = [3]int{1,2,3}			// 使用指定初始值初始化数组

	var c = [...]int{1,2,3,4,5}		// 编译器根据初始值的数量自行推断数组的长度

	d := [...]int{1: 1, 3: 5}		// 只初始化部分值
}
  • 数组的遍历
func main() {
	array := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	// for循环遍历
	for i := 0; i < len(array); i++ {
		fmt.Println(array[i])
	}

	// for range遍历
	for index, value := range array {
		fmt.Println(index, value)
	}
}
  • 多维数组
func main() {
	a := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
	b := [2][4]string{
		{"1", "2", "3", "4"},
		{"5", "6", "7", "8"},
	}

	for _, v1 := range b {			// 获取内层数组
		for _, v2 := rangge v1 {	// 遍历内层数组
			fmt.Println(v2)
	}

	// 多维数组只有第一层可以让编译器自动推导数组长度
	c := [...][2]string{
		{"1", "2"},
		{"3", "4"},
		{"5", "6"}
	}
}

切片

切片是可以存储相同类型元素长度可变的序列,是基于数组类型封装的,就像一个长度可变的滑动窗口在底层数组上移动,可以包含数组的全部或部分元素

多个切片可能对应同一个底层数组,切片之间可以重叠,因此修改切片时,有可能影响拥有相同底层数组的其他切片

一个完整切片需要包含三个要素

  1. 起始元素的地址:指切片中的底层数组中第一个元素的地址

  2. 长度len:指切片中元素的数量,内置len函数获取切片长度

  3. 容量cap:指切片中第一个元素到可能达到的最后一个元素的数量,内置cap函数获取切片容量

  • 创建切片
  1. 切片字面量
func main() {
	a := []int{1, 2, 3}
	b := []string{"1", "2", "3"}
	c := []int{1, 2, 3, 99: 100}	// 使用索引方式指定初始值
	fmt.Println(len(c), cap(c))		// 100 100
}
  1. 切片表达式

基于数组通过切片表达式得到切片,索引low和high必须满足0 <= low <= high <= len(array),否则会索引越界,左闭右开

由于切片的底层是数组,因此对数组取切片得到的是数组,也即是一个切片,因此存在容量cap的概念;而对字符串取切片得到的是字符串,没有容量cap的概念

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s1 := a[1:3]		// [2 3]
	fmt.Printf("s1: %v type: %T len: %v cap: %v\n", s1, s1, len(s1), cap(s1))	// [2 3] []int 2 4,长度为2容量为4

	// 可省略
	a[2:]		// a[2:len(a)]
	a[:3]		// a[0:3]
	a[:]		// a[0:len(a)]

	b := "hello world"
	s2 := b[1:3]		// "el"
	fmt.Printf("s2: %v type: %T len: %v cap: %v\n", s2, s2, len(s2), cap(s2))	// cap(s2)报错,字符串类型没有容量的概念
}

对切片取切片得到的是切片,此时high的上限是切片的容量cap(a)

func main() {
	a := [5]int{1, 2, 3, 4, 5}

	s := a[1:3] // [2, 3]

	s1 := s[3:4] // 切片的切片,high可以到切片的容量

	fmt.Printf("s1: %v len: %v cap: %v\n", s1, len(s1), cap(s1)) // s1: [5] len: 1 cap: 1
}

完整切片表达式,除了low和high,增加一个max用来表示结果的容量,此时会将结果切片的容量设置为max-low,字符串不支持完整切片表达式,需要满足0 <= low <= high <= max <= len(array),否则会索引越界

func main() {
	a := [5]int{1, 2, 3, 4, 5}

	s := a[1:3:4] // 额外指定max,控制切片容量

	fmt.Printf("s: %v len: %v cap: %v\n", s, len(s), cap(s)) // [2, 3] len: 2 cap: 3

	s2 := s[3:4]

	fmt.Printf("s2: %v len: %v cap: %v\n", s2, len(s2), cap(s2)) // 报错,由于s指定了容量3,此时s已经没有包含5这个元素了

	s1 := a[1:3]

	fmt.Printf("s1: %v len: %v cap: %v\n", s1, len(s1), cap(s1)) // [2, 3] len: 2 cap: 4
}
  1. make创建切片

用于动态创建一个切片,使用make函数可以将切片所需容量一次申请到位,切片的元素数量小于或等于该切片的容量

func main() {
	s1 := make([]string, 2)			// cap默认与长度相同,len2,cap2
	s2 := make([]string, 2, 10)		// len2, cap10
}
  • 空切片

nil切片不仅没有大小和容量,也没有指向任何底层的数组

empty切片没有大小和容量,但是指向了一个特殊的内存地址zerobase,即所有0字节分配的基地址

要判断一个切片是否为空,只能用len(s) == 0

切片中的元素不是直接存储的值,因此不允许切片之间直接使用==进行比较,切片唯一合法的比较操作是和nil比较

func main() {
	var s1 []int			// nil切片
	s2 := []int{}			// empty切片
	s3 := make([]int, 0)	// empty切片
}
  • 切片操作
  1. 切片遍历
func main() {
	s := []int{1, 2, 3}
	// 索引遍历
	for i := 0; i < len(s); i++ {
		fmt.Println(s[i])
	}

	// range遍历
	for index, value := range s {
		fmt.Println(index, value)
	}
}
  1. 切片追加

append函数支持在切片末尾追加一个,也可以一次性追加多个,也可以追加另一个切片中的元素,但是需要配合...符号将被添加的切片展开

Go编译器不允许调用append函数后不适用其返回值,因此通常使用原变量接收append函数的返回值

func main() {
	s1 := []int{1, 2, 3}
	s2 := []int{4, 5, 6}
	s1 = append(s1, 7)		// [1 2 3 7]
	s2 = append(s2, 8, 10)	// [4 5 6 8 10]
	s1 = append(s1, s2...)	// [1 2 3 7 4 5 6 8 10]

	// append函数中可以直接使用nil slice
	var s []int  			// nil slice
	s = append(s, 1, 2, 3)

	// 使用make时要注意是否初始化时指定初始大小
	s3 := make([]int, 3)
	s3 = append(s3, 1, 2, 3)		// [0 0 0 1 2 3]

	s4 := make([]int, 0, 3)
	s4 = append(s4, 1, 2, 3)		// [1 2 3]
}
  1. 切片扩容策略

向切片中添加元素时,若底层数组不能容纳新增的元素,切片会自动按照一定的策略进行扩容,此时切片指向的底层数组会更换

  • Go1.21.1版本的切片扩容策略

    • 若newLen > doublecap,则newcap = newLen
    • newLen <= doublecap,并且oldCap < 256,则newcap = doublecap
    • oldCap >= 256,则最终容量newcap开始循环增加(newcap + 3 * threshold) / 4,直到newcap >= newLen
  1. 切片复制拷贝

简单的复制会导致两个切片共享同一个底层数组,此时对一个切片进行修改会影响另一个切片的内容

func main() {
	s1 := make([]int, 3)
	s2 := s1 				// 将s1赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100				// 修改s2的值,s1的值也会变
	fmt.Println(s1, s2)		// [100 0 0] [100 0 0]
}

可以使用copy函数将一个切片的数据复制到另一个切片空间,此时两个切片不会共享同一个底层数组,因此可以安全的对一个切片进行修改而不会影响另一个切片

func main() {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := make([]int, 5, 5)

	copy(s2, s1)				// 将切片s1中的元素复制到切片s2中
}
  1. 切片删除元素

Go语言没办法删除切片中间某个元素并保持剩余元素顺序的办法,只能通过切片操作将删除的元素前后的元素顺序连接起来

func main() {
	s := []int{1, 2, 3}
	// 删除索引1元素
	s1 := append(s[:1], s[2:]...)

	s2 := copy(s[:1], s[2:])
}

映射map

Go的映射是一种无序的键值对类型,一个映射的所有键都必须是相同的类型,值也必须是相同的类型,但是键和值的类型可以不同

只有能使用==操作符进行比较的类型才能作为映射的键,任何类型都可以作为映射的值,不建议使用浮点型作为映射的键

  • 创建映射
  1. 使用映射字面量的方式创建携带初始值的映射变量

默认初始值为nil,不能直接对nil映射添加元素,因为还没有初始化

  1. 使用make函数创建映射

使用make函数创建映射时,不管有没有指定容量,初始值都不再是nil

使用make函数创建映射时,容量参数不是必须的,映射会根据需要自动扩容,但是我们应该在初始化映射时就指定一个合适的容量,这样能有效减少运行时的内存分配,提高代码的执行效率

func main() {
	// 默认初始值为nil
	var m1 map[string]string

	m1["zhangsan"] = "zhangsan" // 报错

	m2 := map[string]string{
		"zhangsan": "zhangsan",
		"lisi":     "lisi",
	}

	// 默认初始值不为nil
	m3 := make(map[string]string)

	m3["zhangsan"] = "zhangsan" // 正常
}
  • 映射的基本使用

访问、删除、判断键是否存在、遍历

可以直接使用map[key]的方式访问元素,若key不存在,返回值类型对应的零值

可以使用len函数来获取映射中的元素数量

Go内置delete函数用来从映射中删除一组键值对

虽然通过map[key]访问映射中的元素时,若键不在映射中会返回对应类型的零值,但是这样不太友好,Go语言中有专门的写法来判断映射中是否存在某个键value, ok := map[key]

同样可以使用range来遍历映射的键和值,但是映射的遍历结果是不固定的,若要按照指定的顺序遍历映射,则可以先将映射的所有键取出放到一个切片中,再对切片进行排序,将遍历该切片的元素作为键去访问映射,间接实现按顺序访问的效果

func main() {
	m := map[string]string{
		"zhangsan": "zhangsan",
		"lisi":     "lisi",
	}
	fmt.Println(len(m)) // 2
	// 添加元素
	m["zhangsi"] = "zhangsi"
	// 访问元素
	fmt.Println(m["zhangsan"])
	fmt.Println(len(m)) // 3

	// 删除元素
	delete(m, "zhangsi")

	// 判断键值对是否存在
	value, ok := m["zhangsi"]

	// 按顺序遍历映射
	// 先取出所有key存入切片中
	keys := make([]string, 0, len(m))
	for key, _ := range m {
		keys = append(keys, key)
	}
	// 对切片进行排序
	sort.Strings(keys)
	// 按顺序遍历映射
	for _, key := range keys {
		fmt.Println(key, m[key])
	}
}
  • 切片作为映射的值

感觉非常适合用来存储一些人员信息,比如一个切片就是一个人员,这个切片中可以存放很多个映射的数据

func main() {
	// 映射类型的切片
	m := make([]map[string]string, 0, 10)

	// 对切片中的映射元素进行初始化
	m[0] = make(map[string]string, 2)
	m[0]["a"] = "a"
	m[0]["b"] = "b"
}
  • 映射类型的切片
func main() {
	// 值为切片类型的映射
	// 键为字符串,值为字符串切片的映射
	m := make(map[string][]string, 3)
	key := "zhangsan"
	value, ok := m[key]
	if !ok {
		fmt.Println("not found")
	}
	// 找到的话,值就是一个切片
	value = append(value, "lisi")
	m[key] = value
}

函数

函数使用func关键字声明,func 函数名 (参数) (返回值)Go语言支持多个返回值

Go中函数调用过程传参是按值传递,函数接收到的是复制的实参副本,而当传入函数的实参为某些特定类型(指针、切片、映射、函数、通道)时,函数接收到的是实参的引用,此时在函数中修改形参将对实参造成影响

  • 函数的一些特殊用法
  1. 类型简写

当参数中相邻变量的类型相同时可以省略参数的类型,func Sum(x, y int) int {}

  1. 可变参数

指函数的参数数量不固定,Go中可变参数通过在形参后添加...来标识,通常作为函数的最后一个参数,可变参数本质上是一个切片

// 有多个返回值时,在函数声明中必须用括号将两个返回值的类型括起来
func sum(x int, y ...int) (int, int) {
	summ := 0
	for _, v := range y {		// y是一个切片
		summ += v
	}
	return x, summ
}

res1 := sum(10)
res2 := sum(10, 20)
res3 := sum(10, 20, 30)
fmt.Println(res1, res2, res3)	// 10 30 60
  1. 命名返回值与特殊返回值

函数定义阶段可以直接给返回值命名,并在函数体中直接使用这些变量

在某些场景中,nil可以作为特殊返回值返回

func sum(x int, y int) (sum int) {
	sum = x + y
	return
}

func someFunc(x string) []int {
	if x == "" {
		return nil	// 没必要返回[]int{}
	}
	return nil
}
  • 变量作用域

全局变量定义在函数外部,在整个程序运行周期都有效

局部变量又分为:函数内局部变量和语句块局部变量

若局部变量与全局变量重名,则优先访问局部变量(就近原则)

  • 函数类型与变量

函数的类型通常称为函数签名,拥有相同参数列表和返回值列表的函数类型相同

// 以下三个函数,函数签名相同
func f1(x int, y int) int {}
func f2(x, y int) int {}
func f3(x, y int) (res int) {}

Go语言中函数也可以是一种类型,可以将函数类型的值保存在变量中,函数类型的零值为nil

func sayHello(name string) {
	fmt.Println("Hello,", name)
}

func main() {
	var f func(string)			// 声明函数类型变量f
	f = sayHello				// 将sayHello赋值给f
	f("Go")						// 输出:Hello, Go
}

可以使用type关键字为函数类型定义类型别名

如,定义了一个calculation类型,他是func(int, int) int类型的别名,只要函数签名满足func(int, int) int,那么该函数就可以赋值给calculation类型的变量,并进行调用

type calculation func(int, int) int
func add(x, y int) int {
	return x + y
}
func sub(x, y int) int {
	return x - y
}

func main() {
	var c calculation
	c = add
	fmt.Println(c(1, 2))		// 相当于调用add(1, 2)

	s := sub
	fmt.Println(s(1, 2))		// 相当于调用sub(1, 2)
}
  • 高阶函数

指以其他函数为参数,或返回一个函数作为结果的函数

  1. 函数作为参数
func add(x, y int) int {
	return x + y
}
func sub(x, y int) int {
	return x - y
}

// 将两个整数传入给定op函数进行计算并返回结果
func calc(x, y int, op func(int, int) int) int {
	return op(x, y)
}

func main() {
	addres := calc(10, 20, add)		
	subres := calc(10, 20, sub)
}
  1. 函数作为返回值
func do(s string) (func(int, int) int, error) {
	switch s {
	case "+":
		return func(i, j int) int { return i + j }, nil
	case "-":
		return func(i, j int) int { return i - j }, nil
	case "*":
		return func(i, j int) int { return i * j }, nil
	default:
		err := errors.New("invalid operator")
		return nil, err
	}
}

func main() {
	f, err := do("+")
	if err != nil {
		fmt.Println(err)
		return
	}
	res := f(1, 2)
	fmt.Println(res)
}
  • 匿名函数和闭包

匿名函数可以看作没有函数名的函数,因此没办法像普通函数那样被调用,需要保存到某个变量中或者作为立即执行函数

匿名函数多用于实现回调函数和闭包

func main() {
	// 将匿名函数保存到变量中
	add := func(x, y int) {
		fmt.Println(x + y)
	}
	add(10, 20)

	// 自执行函数:匿名函数定义玩笔加()直接执行
	func(x, y int) {
		fmt.Println(x + y)
	}(10, 20)
}

闭包指一个函数和与其相关的引用环境组合而成的实体,即闭包=函数+引用环境,即闭包是一个引用了外层函数局部变量的函数

func addrer() func(int) int {
	var x int
	return func(y int) int {
		// 将y的值累加到其外层函数的局部变量x
		x += y
		return x
	}
}

// 闭包的x变量也可以是外层函数的参数,在函数调用时由外部传入,更加灵活
func addrer2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}

func main() {
	var f = addrer()			// 在f的作用域中,闭包中的x也一直有效
	fmt.Println(f(1))			// 1	
	fmt.Println(f(2))			// 3
	fmt.Println(f(3))			// 6
}

一个闭包使用的实际性例子,理解上来说,可以实现对一个函数的复用,更常见的做法是将外部变量传入闭包中,从而使得该闭包在其生命周期内都都与该外部变量有关

// makeSuffixFunc, 返回一个给文件名添加指定后缀的函数
func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		// 判断文件名是否包含指定后缀
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

func main() {
	// 创建一个添加.jpg后缀的函数
	jpgFunc := makeSuffixFunc(".jpg")
	// 创建一个添加.txt后缀的函数
	txtFunc := makeSuffixFunc(".txt")
	// 给文件名avatar添加.jpg后缀
	fmt.Println(jpgFunc("avatar")) // avatar.jpg
	// 给文件名readme添加.txt后缀
	fmt.Println(txtFunc("readme")) // readme.txt
}
  • 内置函数
  1. defer函数

defer语句能够延迟函数调用,这个被延迟调用的函数的所有返回值都需要被丢弃

被延迟处理的语句将按defer定义的逆序执行,即先被defer的语句最后被执行,最后被defer的语句最先被执行,类似于栈先进后出

defer函数常用于在读取文件内容时,延迟执行f.Close(),能够保证在函数运行玩笔后妥善地关闭文件,释放相关资源

func readFromFile(filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()		// 函数结束时关闭文件
	return nil
}

defer函数也经常用来做一些调试工作,如在函数的入口和出口执行特定的语句,记录执行时间和相关变量等

func recordTime(funcName string) func() {
	start := time.Now()
	fmt.Printf("enter %s\n", funcName)
	return func() {
		fmt.Printf("exit %s, spend %v\n", funcName, time.Since(start))
	}
}

func readFromFile(filename string) error {
	defer recordTime("readFromFile")() // defer延迟调用,虽然recordTime函数return的没有变量进行接收,但是也会执行打印输出
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close() // 函数结束时关闭文件
	return nil
}

return语句不是原子操作,包含两步,首先是返回赋值,再执行RET指令进行返回,而defer的执行处于这两个步骤之间,因此可以更新函数的返回值变量

// 首先将返回值赋给返回变量,接着修改x的值,最后返回,因此不会更新返回值
func f1() int {
	x := 5
	defer func () {
		x++				// 只修改x的值,不会更新返回值
	}()

	return x			// 返回值=5
}

// 首先x=5,然后执行defer会更新x的值,最后返回,因此此时会更新返回值,因为提前声明好了返回值变量
func f2() (x int) {
	defer func() {
		x++				// 会更新返回值
	}()
	return 5			// 返回值 x = 5
}
  1. panic函数

当一个函数调用发生panic时,会立即进入退出阶段,开始执行所有延迟调用函数,然后程序会异常退出并输出相应的日志,日志中会包含函数调用信息和具体的panic值

panic函数可以接收任意值作为参数,但通常使用相关错误信息作为参数

panic会导致程序异常退出,因此一般只在程序出现不应该出现的逻辑错误时才会使用,应该使用Go语言提供的错误机制,而不是滥用panic机制

对于一个函数其非法的情况是不可能出现的,因此通常我们没必要去处理这种不可能出现的错误,此时就会用panic,并约定熟成的使用Muust作为函数名称的前缀

// Compile函数是regexp包中用于将字符串参数编译成正则表达式的函数
// 正常情况下我们不会在代码中添加一个非法的正则表达式,因此对于错误情况直接使用panic进行处理
func MustCompile(str string) *Regexp {
	regexp, err := Compile(str)
	if err != nil {
		panic(`regexp: Compile(`+ quote(str) +`): ` + err.Error())
	}
	return regexp
}
  1. recover函数

程序可以使用recover函数从panic状态恢复,通过在延迟调用函数中调用recover函数,可以消除当前goroutine中的一个panic,回到正常状态;recover函数旨在defer调用的函数中有效

在一个处于panic状态的goroutine退出之前,其中的panic不会蔓延到其他的goroutine;如果一个goroutine在panic状态下退出,则会使整个程序崩溃

func funA() {
	fmt.Println("funA")
}

func funB() {
	// 延迟调用一个自执行的函数,必须得是panic了之后再恢复,因此recover必须配合defer
	defer func() {
		err := recover()
		// 如果程序出现了panic,那么可以通过recover恢复
		if err != nil {
			fmt.Printf("recover from panic: %v\n", err)
		}
	}()

	panic("funB panic")
}

func funC() {
	fmt.Println("funC")
}

func main() {
	funA()
	funB()
	funC()
	/*
		funA
		recover from panic: funB panic
		funC
	*/
}

使用recover函数从panic中恢复时需要特别注意以下几点:

①一定要在可能引发panic的语句之前调用recover函数

②recover函数必须搭配defer使用,并且不能直接使用defer延迟调用

③recover函数只能恢复当前goroutine的panic

除非明确知道程序应该使用panic和recover,否则一般不要使用它们

  1. new和make函数

当我们声明一个值类型的变量时,Go语言会默认已经为其分配内存,而当我们声明指针或包含指针类型的变量时,不仅要声明类型,还要为它申请内存空间,否则没办法直接存储值,此时就要用到new和make函数

new函数不太常用,其分配内存后得到的是一个类型的指针,该指针对应的值就是该类型的零值

func main() {
	a := new(int)		// 得到的是int类型的指针
	b := new(bool)		// 得到的是bool类型的指针
}

make函数更常用,但是他只用于切片、映射和通道三个类型,并且返回值也是这三个类型本身,而不是他们的指针类型

在使用切片、映射、通道时,都需要先使用make函数对他们进行初始化

func main() {
	var s []int
	s = make([]int, 1) 			// make函数的返回值就是一个切片,分配一个内存
	s[0] = 100
	s = append(s, 200)			// 不会报错,切片会自动扩容
	fmt.Println(s[0], s[1])

	var m map[string]int
	m = make(map[string]int, 1) // make函数的返回值就是一个映射,分配两个内存
	m["zhangsan"] = 100

	m["zhangsi"] = 100 			// 不会报错,映射会自动扩容

	for k, v := range m {
		fmt.Println(k, v)
	}
}

指针

Go的指针不能偏移和运算,是安全指针

func main() {
	// 使用取地址方式创建指针
	v := 10
	p := &v

	// 使用new创建指针
	p1 := new(int)

	// 指针取值
	*p1 = 10
}