Skip to content

定义函数

  1. 使用关键字: func
  2. 函数名要求:
    1. 使用小驼峰(go 语言中 大驼峰有特殊意义)
    2. 不能以数字开头
    3. 尽量见名知意
go
/*
定义函数的语法如下:

func 函数名(参数列表) 返回值类型列表 {
  函数体
  return 返回值列表
}
*/
package main

import (
	"fmt"
)

// 1.定义函数
func sum(n1 int, n2 int) int {
	return n1 + n2
}

// 2.调用函数
func main() {
	result := sum(1, 2)
	fmt.Println("1 + 2 =", result)
}

参数

go
func print(n1 int, n2 int) {
	fmt.Println(n1, n2)
}
go
package main

import "fmt"

// 调用时可以传入任意多个 int 类型的参数
// 会自动收集到一个 int 类型的切片中
func printNumbers(nums ...int) {
	fmt.Println(nums)
}

func main() {
	printNumbers(1, 2, 3, 4)
	// 输出 [1,2,3,4]
}

返回值

go
func printAll(n1 int, n2 int) {
	fmt.Println(n1, n2)
}
go
func sum(n1 int, n2 int) int {
	return n1 + n2
}
go
// 注意返回多少个值, 都要在返回值类型列表中声明
func increment(n1 int, n2 int) (int, int) {
	n1 += 1
	n2 += 1
	return n1, n2
}
go
package main

import "fmt"

func increment(num int) (incInt int, incFloat float32) {
	incInt = num + 1
	incFloat = float32(num) + 0.1

	// 0.return 关键字必须要有
	// 1.return 会自动返回 incInt, decFloat 变量
	// 2.要使用自动返回, 在声明返回值类型的时候必须同时声明变量名和类型
	// 3.在接收函数返回值的时候, 必须按照声明的顺序接收
	return
}

func main() {
	incInt, incFlt := increment(5)
	fmt.Println("incInt=", incInt)   // 6
	fmt.Println("incFloat=", incFlt) // 5.1
}

值传递与引用传递

  • 引用传递的本质就是 (内存)地址传递
go
package main

import "fmt"

func increment(num int) int {
	num += 1
	return num
}

func incrementPtr(numPtr *int) {
	*numPtr += 1
}

func main() {
	// 直接调用函数而不重新赋值是不行的, 因为 increment 是值传递
	// increment(num)
	// 将 num 的值修改为 increment 函数的返回值
	num := 10
	num = increment(num)
	fmt.Println("num is", num)

	// 直接传递 num2 的地址, incrementPtr 函数可以根据地址
	// 直接修改num2 对应内存地址的值, 不需要重新赋值
	num2 := 20
	incrementPtr(&num2)
	fmt.Println("num2 is", num2)
}
go
package main

import "fmt"

func incrementNumbers(nums [5]int) {
	// 在 GO 语言中, 即使是复杂数据类型(包括:数组/struct/channel)
	// 默认情况下都是 "值" 传递
	// 所以在函数内部输出的变量的内存地址和main函数中输出的不一样
	fmt.Printf("[incrementNumbers]数据内容: %v\n", nums)
	fmt.Printf("[incrementNumbers]变量类型: %T\n", nums)
	fmt.Printf("[incrementNumbers]内存地址: %p\n", &nums)

	for i := 0; i < len(nums); i++ {
		nums[i] = nums[i] + 1
	}
}

func incrementNumbersByPtr(nums *[5]int) {
	// 只有在明确指定是使用指针类型的时候, 传递的才是指针
	fmt.Printf("[incrementNumbersByPtr]数据内容: %v\n", nums)
	fmt.Printf("[incrementNumbersByPtr]变量类型: %T\n", nums)
	fmt.Printf("[incrementNumbersByPtr]内存地址: %p\n", &nums)

	// 注: 如果是指针变量, 需要解应用才能获取到原内存存储的值
	for i := 0; i < len(*nums); i++ {
		(*nums)[i] += 1
	}
}

func main() {
	nums := [5]int{1, 2, 3, 4, 5}
	fmt.Printf("[main]内存地址: %p\n", &nums)

	// nums 中的值并没有改变, 因为是值传递
	// 在函数内部修改的是 nums 的副本(内存地址都不一样)
	incrementNumbers(nums)
	fmt.Printf("[main]数据内容: %v\n", nums)

	// nums 中的值被改变了, 因为是引用用传递
	// 在函数内部修改的 nums 是通过内存地址找到的原件
	incrementNumbersByPtr(&nums)
	fmt.Printf("[main]数据内容: %v\n", nums)
}
go
package main

import "fmt"

// 引用传递
func incrementNumbers(nums []int) {
	for i := range nums {
		nums[i] += 1
	}
}

func main() {
	nums := []int{1, 2, 3, 4, 5}
	incrementNumbers(nums)
	fmt.Printf("数据:%v  类型:%T\n", nums, nums)

	// 为什么数组是值传递, 而切片却是引用传递?
	// 0.数组的大小是固定的,它的数据存放在栈道
	// 1.切片的大小是不确定的,所以数据一定存放在堆区
	// 2.可以简单的理解为: 存在堆区的数据必须是引用传递的
  // 3.映射表 map 同理可知
}

函数的类型

go
package main

import "fmt"

func increment(num int) int {
	num += 1
	return num
}

func main() {
	// 注意: 我并没有用()调用这个函数
	inc := increment
	fmt.Printf("inc 的类型是: %T, increment 的类型是 %T", inc, increment)
	// 输出如下:
	// inc 的类型是: func(int) int, increment 的类型是 func(int) int

	// 通过变量调用函数
	result := inc(10) // 就相当于执行了: increment(10)
	fmt.Println("\nresult 的值是: ", result)
}

将函数类型作为参数

既然函数可以赋值给一个变量, 那么函数就可以作为参数传递给另外一个函数

go
package main

import "fmt"

func each(count int, handler func(int)) {
	for i := range count {
		handler(i)
	}
}

func printNumber(num int) {
	fmt.Println("num =", num)
}

func main() {
	// 传入 printNumber 函数作为参数
	each(5, printNumber)

	fmt.Println("---------------")

	// 直接传入匿名函数作为参数
	each(10, func(i int) {
		fmt.Println("i =", i)
	})
}

匿名函数

就是没有名称的函数类型的值, 通常用于赋值给变量, 或者当作参数传递

go
package main

import "fmt"

func main() {
	// 将一个匿名函数赋值给一个变量
	printNumber := func(num int) {
		fmt.Println("num =", num)
	}

	// 通过变量名调用函数
	printNumber(10)
}

内置函数

所谓的内置函数(也叫内建函数)其实就是 builtin 这个包中所有的函数, 可以直接使用, 不需要导入 它的源码放在 go 语言安装目录的 go/src/1.25.5/builtin/builtin.go 这个文件中

go
package main

func main() {
  // 这个 println 就是内置函数
  // 但是 ctrl+鼠标左键 点击进去查看源码会发现
  // 这个文件中, 只有注释和类型定义, 并没有实现,
  // 其实最后, 还是使用的 fmt.Println 函数
  println("hello world")
}

作用域

在 Go 中, 作用域分为:

  • 全局作用域局部作用域
  • 局部作用域也叫 函数作用域
go
package main

import "fmt"

// 全局作用域
var step int = 1

func increment(num int) int {
	// 在这个函数中可以访问全局作用域的变量
	res := num + step

	// 局部作用域:
	// 只有在 increment 函数中才能访问 res 变量
	return res
}

func main() {
	// 在这个函数中可以访问全局作用域的变量
	fmt.Println("step:", step)
	fmt.Println("increment: ", increment(1))


  // 访问变量就近原则:
  // 如果当前作用域能找到变量, 就使用当前作用域的变量
	step := 10
	fmt.Println("local step:", step) // 10
}

defer 关键字

defer 是 Go 语言中一个非常独特的关键字, 用于延迟函数的执行, 将其推迟到包含它的函数返回之前执行, 这个特性使得资源清理和错误处 理变得更加简洁和可靠, 同时也会带来一些额外的开销

基本用法

go
package main

import "fmt"

func main() {
	// 在函数调用前加 defer 即可
	defer fmt.Println("1.defer fmt.Println executed")

	// 不论是否是匿名函数都可以用
	defer func() {
		fmt.Println("2.defer func executed")
	}()
}

执行顺序 LIFO

多个 defer 语句按照后进先出的顺序执行

go
package main

import "fmt"

func main() {
	defer fmt.Println("1.defer fmt.Println executed")
	defer func() {
		fmt.Println("2.defer func executed")
	}()
  // go run main.go 的输出结果:
  // 2.defer func executed
  // 1.defer fmt.Println executed
}

获取参数值的时机

go
package main

import "fmt"

func main() {
	num := 1

	// 此时不会执行, 但是会获取参数的值
	// 并不是等到执行那一刻才获取参数的值
	defer fmt.Println("1.num = ", num)

	num = 10
	fmt.Println("2.num = ", num)

	// 所以输出的结果是:
	// 2.num =  10
	// 1.num =  1
}
go
package main

import "fmt"

func main() {
	num := 1

	// 如果想要延迟获取, 那么只需要给原语句
	// 套一层匿名函数即可, 此时 defer 修饰的
	// 是这个 func(){}() 匿名函数,
	defer func() {
		// 真正执行时才会获取 num 的值
		fmt.Println("1.num = ", num)
	}()

	num = 10
	fmt.Println("2.num = ", num)

	// 所以输出结果是:
	// 2.num =  10
	// 1.num =  10
}

错误处理

defer + recover 捕获错误

在go语言中, 没有 try catch 关键字, 那么它是如何管理 可预测的代码异常的

go
package main

import "fmt"

func test() {
	num1 := 10
	num2 := 0
	result := num1 / num2 // 除 0 会报错
	// panic: runtime error: integer divide by zero
	// goroutine 1 [running]:
	// main.main()
	//         /Users/secret/codes/golang-demo/main.go:8 +0x9
	// exit status 2

	// 这一行不会执行
	fmt.Printf("%d / %d = %d \n", num1, num2, result)
}

func main() {
	test()

	// 后续的代码不会执行
	fmt.Printf("后续的代码不会执行")
}
go
package main

import "fmt"

func test() {
	// defer 一个自执行匿名函数, 让他延迟执行
	defer func() {
		// 捕获错误
		err := recover()
		if err != nil {
			fmt.Println("捕获到代码错误", err)
		}
	}()

	num1 := 10
	num2 := 0
	result := num1 / num2

	// 这一行不会执行
	fmt.Printf("%d / %d = %d \n", num1, num2, result)
}

func main() {
	test()
	fmt.Println("后续代码执行了")
  // 控制台输出如下:
  // 捕获到代码错误 runtime error: integer divide by zero
  // 后续代码执行了
}

自定义错误与错误的类型

go
package main

import (
	"errors"
	"fmt"
)

// 1.能够这样设置返回值的类型说明:
// err 是 error 类型, 代表一个具体的错误
// nil 是 error 类型, 表示没有任何的错误
func test(x int) error {
	if x == 0 {
		// 2.自定义错误信息, 需要使用 errors 包中的 New 方法
		err := errors.New("x 不能为0")
		return err
	}

	return nil
}

func main() {
	err := test(0)
	if err != nil {
		fmt.Printf("err 的类型是: %T \n", err) // err 的类型是: *errors.errorString
	}

	res := test(1)
	fmt.Printf("res 的类型是: %T \n", res) // res 的类型是: <nil>
}

手动终止程序执行

上面的例子, 我们发现, 即使出现错误, 也不会中断程序的执行, 只是正常的返回了一个 error 类型的数据

go
package main

import (
	"errors"
	"fmt"
)

func test(x int) error {
	if x == 0 {
		err := errors.New("x 不能为0")
		return err
	}

	return nil
}

func main() {
	err := test(0)
	if err != nil {
		// 如果程序出现错误, 手动让程序终止执行
		// 相当于其他语言中的手动 throw 一个异常
		panic(err)
	}

	// 后续的代码不会再执行了
	res := test(1)
	fmt.Printf("res 的类型是: %T \n", res)
}

给自定义类型增加处理函数

go
package main

import "fmt"

type Pos [2]int

// GetX/GetY 是定义在 Pos 类型上的方法
// 必须通过 Pos 这个类型的数据来调用

func (p Pos) GetX() int {
	return p[0]
}

func (p Pos) GetY() int {
	return p[1]
}

// GetX/GetY 是普通方法
// 只要是 [2]int 类型就能调用

func GetX(p Pos) int {
	return p[0]
}

func GetY(p Pos) int {
	return p[1]
}

func main() {
	var p Pos = [2]int{10, 20}

	// 通过 Pos 类型调用类型上才有的方法
	x1 := p.GetX()
	y1 := p.GetY()
	fmt.Println("x1, y1:", x1, y1)

	// 通过传入 pos 数据, 调用普通方法
	x2 := GetX(p)
	y2 := GetY(p)
	fmt.Println("x2, y2:", x2, y2)
	fmt.Println("-------------------")

	nums := [2]int{30, 50}
	x3 := GetX(nums)
	y3 := GetY(nums)
	fmt.Println("x3, y3:", x3, y3)

	// 这样是无法调用的:
	// 因为变量 nums 的类型是 [2]int, 而变量 p 的类型是: Pos
	// x4 := nums.GetX()
	// y4 := nums.GetY()

	// 要是想要像上面那样调用需要先转换类型
	p2 := Pos(nums)
	x4 := p2.GetX()
	y4 := p2.GetY()
	fmt.Println("x4, y4:", x4, y4)
}

泛形函数

为什么要有泛形函数

如下3个函数, 除了类型不同, 实现逻辑是一模一样的, 就因为类型不同, 就要定义3个处理逻辑一样函数, 这么多重复代码显然是不太合理的, 是否有类似 TS/Java 的泛型来解决代码高度重复的问题

go
package main

import "fmt"

func filterInt(items []int, filterFunc func(int, int) bool) []int {
	result := make([]int, 0)
	for index, value := range items {
		if filterFunc(value, index) {
			result = append(result, value)
		}
	}
	return result
}

func filterFloat(items []float64, filterFunc func(float64, int) bool) []float64 {
	result := make([]float64, 0)
	for index, value := range items {
		if filterFunc(value, index) {
			result = append(result, value)
		}
	}
	return result
}

func filterString(items []string, filterFunc func(string, int) bool) []string {
	result := make([]string, 0)
	for index, value := range items {
		if filterFunc(value, index) {
			result = append(result, value)
		}
	}
	return result
}

func main() {
	items := []int{1, 2, 3, 4, 5}
	filteredItems := filterInt(items, func(value int, index int) bool {
		return value%2 == 1
	})
	fmt.Println(filteredItems)

	floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
	filteredFloats := filterFloat(floats, func(value float64, index int) bool {
		return value > 3
	})
	fmt.Println(filteredFloats)

	langs := []string{"Java", "Rust", "JavaScript", "TypeScript", "python"}
	filteredLangs := filterString(langs, func(value string, index int) bool {
		return len(value) > 4
	})
	fmt.Println(filteredLangs)
}

使用泛型函数

go
package main

import "fmt"

/*
泛型函数:

	func 函数名[定义泛型名称 泛型允许的类型约束] (参数 使用泛型作为参数类型) 使用泛型作为返回值类型 {
	  // 在函数体中也可以使用泛型来约束变量的类型
	}
*/
func filter[T int | float64 | string](items []T, filterFunc func(T, int) bool) []T {
	result := make([]T, 0)
	for index, value := range items {
		if filterFunc(value, index) {
			result = append(result, value)
		}
	}
	return result
}

func main() {
	items := []int{1, 2, 3, 4, 5}
	filteredItems := filter(items, func(value int, index int) bool {
		return value%2 == 1
	})
	fmt.Println(filteredItems)

	floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
	filteredFloats := filter(floats, func(value float64, index int) bool {
		return value > 3
	})
	fmt.Println(filteredFloats)

	langs := []string{"Java", "Rust", "JavaScript", "TypeScript", "python"}
	filteredLangs := filter(langs, func(value string, index int) bool {
		return len(value) > 4
	})
	fmt.Println(filteredLangs)
}

泛形类型

所谓的泛型类型其实就是带有泛型的类型定义

go
package main

import "fmt"

// 定一个 "数字数组类型", 具体是哪种数字用泛型

type NumberArray[T int | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64 | float32 | float64] []T

// 给类型绑定求和方法

func (nums NumberArray[T]) sum() T {
	result := T(0) // 转类型
	for _, num := range nums {
		result += num
	}
	return result
}

func main() {
	// 使用类型绑定的函数时, 指定泛型的具体类型为 int
	integers := []int{1, 2, 3, 4, 5}
	result1 := NumberArray[int](integers).sum()
	fmt.Println(result1)

	// 使用类型绑定的函数时, 指定泛型的具体类型为 float64
	floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
	result2 := NumberArray[float64](floats).sum()
	fmt.Println(result2)
}

Released under the MIT License.