Skip to content

函数和指针

函数

在 Go 语言中,函数是一个独立的代码块,用于执行特定的任务或计算。函数可以接收零个或多个参数,并且可以返回零个或多个值。函数的定义包括函数名、参数列表、返回值类型和函数体。

语法格式

go
// 定义一个函数的语法格式如下:
func 函数名(参数列表) (返回值列表) {
    函数体
}

示例

go
// 以下定义了一个函数,这个函数没有参数,没有返回值
func printInfo() {
	fmt.Println("Hello")
	fmt.Println("World")
}

函数的命名规范

在 Go 语言的函数命名规范旨在提高代码的可读性和易理解性,并确保代码风格的一致性。遵循良好的命名规范可以使代码更易于维护和理解,尤其是对于大型项目或多人协作开发的项目。

以下是一些 Go 语言函数命名规范的基本原则:

  • 使用驼峰命名法(Camel Case):函数名应该由多个单词组成,每个单词的首字母大写,除了第一个单词以外。例如:GetUserInfoCalculateAverage 等。
  • 函数名应该具有描述性:函数名应该清晰地表达函数的功能和用途。避免使用过于笼统或模糊的名称,例如:doSomethingprocess
  • 使用动词:函数名通常应该使用动词来表示函数的功能。例如:GetUserInfoWriteToFile 等。
  • 避免缩写:除非是约定俗成的缩写,否则尽量避免在函数名中使用缩写。例如:useDB 优于 useDatabase
  • 区分大小写:Go 语言区分大小写,因此函数名中的大小写也应该有意义。例如:GetUserInfogetUserInfo 是两个不同的函数。
  • 包内函数和小写开头:如果函数仅供包内使用,则函数名应该以小写开头。例如:getUserInfo。如果函数需要供包外访问,则函数名应该以大写开头。例如:GetUser
  • 特殊函数命名:一些特殊类型的函数遵循特定的命名规范,例如:
    • 测试函数:通常以 Test 开头,后面是所测试的功能或方法的名称。例如,TestGetUserInfo
    • 基准测试函数:通常以 Benchmark 开头,后面是所测试的功能或方法的名称。例如,BenchmarkGetUserInfo
    • 示例函数:通常以 Example 开头,后面是所演示的功能或方法的名称。例如,ExampleGetUserInfo
  • 遵循项目约定:如果项目中存在约定俗成的命名规范,则应遵循项目约定。

函数使用

使用函数时,只需要调用函数名并传递相应的参数。

示例

go
package main

import "fmt"

func main() {
	// 使用函数
	res := add(10, 20)
	fmt.Println(res)
}

// 定义一个函数用于计算两个 int 类型的数字之和
func add(a, b int) int {
	return a + b
}

函数的参数

形式参数:在函数定义时就定义好的参数,用于接收外部传入的数据,叫做形式参数,简称形参。

实际参数:调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。

示例

go
package main

import "fmt"

func main() {
	// 定义两个变量
	p1 := 20
	p2 := 30
	// 调用函数传入的p1,p2就是实际参数
	// 传入的p1就是函数的参数a,传入的p2就是函数的参数b
	// 注意事项:实参与形参必须一一对应:顺序,个数,类型
	res := add(p1, p2)
	fmt.Println(res)
}

// 定义一个函数用于计算两个 int 类型的数字之和
// 定义函数时 a,b 两个就是形式参数
func add(a, b int) int {
	return a + b
}
go
package main

import (
	"fmt"
)

func main() {
	// 调用函数
	// 1-10的和
	getSum(10)
	// 1-50的和
	getSum(50)
	// 1-100的和
	getSum(100)
}

// getSum 定义一个范围求和函数
// 参数n:求和的范围,传入10就是求1-10的和,传入50就是求1-50的和
func getSum(n int) {
	sum := 0
	for i := 1; i <= n; i++ {
		sum += i
	}
	fmt.Printf("1-%d的和是: %d\n", n, sum)
}

在 Go 语言中可变参数函数是一种允许函数接收不定数量参数的方法,使用可变参数可以让函数更加灵活和通用。

语法格式

go
// 在函数定义中,通过在参数类型前加上省略号...来表示该参数是可变参数。
func sum(nums ...int) int

示例

go
/*
	这种参数写法是可变参数
	add()函数受不定数量的参数,参数的类型全部是int类型
	add(1,2,3)
	add(4,5,6,7,8)
*/
func add(args ...int) {
	sum := 0
	for _, v := range args {
		sum += v
	}
	fmt.Println("传入的数值之和:", sum)
}

函数返回值

在 Go 语言中通过 return 关键字向外输出返回值。

示例

go
func add(a,b int) int {
	return a + b
}

Go 语言中函数支持多返回值,函数如果有多个返回值时必须用 () 将所有返回值包裹起来。

示例

go
func demo(a, b int) (int, int) {
	res1 := a + b
	res2 := a - b
	return res1, res2
}

在函数定义时可以给出参起别名,并在函数体中直接使用这些别名,最后通过 return 关键字返回函数的返回值。

示例

go
func demo(a, b int) (res1 int, res2 int) {
	res1 = a + b
	res2 = a - b
	return
}

可以使用 _ 来忽略不需要的函数返回值。

go
func foo() (int, int) {
    return 1, 2
}

func main() {
	// 这个main函数是foo函数的调用方
	_, b := foo() // 这里我忽略了foo函数返回的第一个值
}

nil 函数

函数也是一种类型,函数变量的默认值是 nil,执行nil函数会引发panic。

示例:

go
var f func()
// f是一个函数类型,值是nil
// 编译正常,运行报错panic: runtime error: invalid memory address or nil pointer dereference
f()

函数的高级用法

在 Go 语言中函数作为一种派生数据类型(复合数据类型),可以看做是一种特殊的变量,用于函数的参数传递和函数的返回值。

  1. 函数作为另一个函数的实际参数传递:在函数中,我们可以将函数作为参数传递给其他函数。
go
package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4, 5}
	// 将 double 函数作为参数传递给 applyOperation 函数
	applyOperation(numbers, double)
}

// 定义一个接受函数作为参数的函数
func applyOperation(nums []int, operation func(int) int) {
	for _, num := range nums {
		result := operation(num)
		fmt.Printf("Operation result: %d\n", result)
	}
}

// 示例函数:加倍操作
func double(n int) int {
	return n * 2
}
  1. 函数作为另一个函数的返回值:在函数中,我们可以将函数作为返回值返回。
go
package main

import "fmt"

// 返回一个函数的函数
func multiplier(factor int) func(int) int {
	// 返回一个闭包函数
	return func(n int) int {
		return n * factor
	}
}

func main() {
	// 创建一个乘以 3 的函数
	times3 := multiplier(3)

	// 使用这个函数来乘以不同的数值
	result1 := times3(5) // 5 * 3 = 15
	result2 := times3(10) // 10 * 3 = 30

	fmt.Println("Result 1:", result1)
	fmt.Println("Result 2:", result2)
}

init 函数

在Go语言中 init 函数是一种特殊的函数,用于在程序执行开始前,自动执行初始化任务。init 函数没有参数和返回值,并且不能被显式调用或引用。它可以存在于任何包(包括 main 包)中,多个 init 函数可以存在于同一个包中,按照它们声明的顺序依次执行。

特殊和用途:

  1. 自动执行:init 函数会在程序执行开始前自动调用,无需手动调用。
  2. 无参数和返回值:init 函数没有参数和返回值。
  3. 包级别的初始化:每个包可以有多个 init 函数,它们按照声明顺序依次执行,用于执行包级别的初始化任务。
  4. 执行顺序:对于一个包,其 init 函数的执行顺序是由包引用的依赖关系决定的,先执行被依赖的包的 init 函数。
  5. 主程序的 main 函数执行前:init 函数通常用于初始化包级别的变量、执行复杂的初始化操作或者注册某些需要在程序运行前执行的逻辑。

示例:

go
package main

import (
    "fmt"
)

func init() {
    fmt.Println("Initializing main package...")
}

func init() {
    fmt.Println("Initializing again...")
}

func init() {
    fmt.Println("Yet another initialization...")
}

func main() {
    fmt.Println("Main function started.")
}

函数的初始化顺序

我们从程序逻辑结构角度来看,Go 包是程序逻辑封装的基本单元,每个包都可以理解为是一个“自治”的、封装良好的、对外部暴露有限接口的基本单元。一个 Go 程序就是由一组包组成的,程序的初始化就是这些包的初始化。每个 Go 包还会有自己的依赖包、常量、变量、init 函数(其中 main 包有 main 函数)等。

在平时开发中,我们在阅读和理解代码的时候,需要知道这些元素在在程序初始化过程中的初始化顺序,这样便于我们确定在某一行代码处这些元素的当前状态。

下面,我们就通过一张流程图,来了解 Go 包的初始化次序:

函数的初始化顺序

这里,我们来看看具体的初始化步骤。

首先 main 包依赖 pkg1pkg4 两个包,所以第一步,Go 会根据包导入的顺序,先去初始化 main 包的第一个依赖包 pkg1

第二步 Go 在进行包初始化的过程中,会采用 “深度优先” 的原则,递归初始化各个包的依赖包。在上图里,pkg1 包依赖 pkg2 包,pkg2 包依赖 pkg3 包,pkg3 没有依赖包,于是 Go 在 pkg3 包中按照 “常量 -> 变量 -> init 函数” 的顺序先对 pkg3 包进行初始化;

紧接着,在 pkg3 包初始化完毕后,Go 会回到 pkg2 包并对 pkg2 包进行初始化,接下来再回到 pkg1 包并对 pkg1 包进行初始化。在调用完 pkg1 包的 init 函数后,Go 就完成了 main 包的第一个依赖包 pkg1 的初始化。

接下来,Go 会初始化 main 包的第二个依赖包 pkg4pkg4 包的初始化过程与 pkg1 包类似,也是先初始化它的依赖包 pkg5,然后再初始化自身;

然后,当 Go 初始化完 pkg4 包后也就完成了对 main 包所有依赖包的初始化,接下来初始化 main 包自身。

最后,在 main 包中,Go 同样会按照 “常量 -> 变量 -> init 函数” 的顺序进行初始化,执行完这些初始化工作后才正式进入程序的入口函数 main 函数。

匿名函数(闭包)

在 Go 语言中,匿名函数是一种没有函数名的函数字面量,也称为闭包(Closure)。它可以在定义的同时被直接调用,或者被赋值给变量后稍后调用。匿名函数通常用于需要临时定义和使用的小型功能块,或者作为函数参数传递给其他函数。

语法格式:

go
func (参数列表) 返回值 {
	// 函数的代码逻辑
}(实际参数)

示例:

go
// 正常的函数
func f1() {
	fmt.Println("我是f1()函数...")
}

// 匿名函数,又称为闭包
func () {
	fmt.Println("这是一个匿名函数")
}()
go
package main

import "fmt"

func main() {
	// 调用f1函数
	f1()
	// 匿名函数,在main函数中只会执行一次,但是使用函数变量的方式可以执行多次
	// 定义一个函数变量
	var f2 func()
	f2 = func() {
		fmt.Println("这是一个匿名函数...")
	}
	// 打印数据类型
	fmt.Printf("f2:%T\n", f2)
	// 调用匿名函数
	f2()
	f2()

	// 定义一个有返回类型的匿名函数
	res1 := func(a, b int) int {
		return a + b
	} // 这里并没有传入参数,所以就是将匿名函数赋值给res1变量
	fmt.Printf("res1数据类型:%T\n", res1)
	res2 := func(a, b int) int {
		return a + b
	}(5, 6) // 这里传入了参数进行了函数的调用,会将结果的数据赋值给res2
	fmt.Printf("res2数据类型:%T\n", res2)
}

// 正常的函数
func f1() {
	fmt.Println("这是f1()函数...")
}
go
package main

import "fmt"

// 函数 `process` 接受一个整数和一个回调函数作为参数
func process(num int, callback func(int)) {
    // 调用回调函数,将 num 作为参数传递给它
    callback(num)
}

func main() {
    // 调用 process 函数,传递一个整数和一个匿名函数作为回调
    process(10, func(num int) {
        fmt.Println("Processing number:", num)
    })
}
go
package main

import "fmt"

// 函数 `counter` 返回一个匿名函数
func counter() func() int {
    count := 0 // count 是闭包内部的变量

    // 返回一个匿名函数,该函数可以访问并修改 count
    return func() int {
        count++
        return count
    }
}

func main() {
    // 创建一个新的计数器
    c1 := counter()

    // 使用 c1 调用返回的匿名函数,每次调用 count 增加 1
    fmt.Println(c1()) // 输出 1
    fmt.Println(c1()) // 输出 2
    fmt.Println(c1()) // 输出 3

    // 创建另一个新的计数器,它拥有自己的 count 变量
    c2 := counter()

    // 使用 c2 调用返回的匿名函数,它从 1 开始计数
    fmt.Println(c2()) // 输出 1
    fmt.Println(c2()) // 输出 2
}

匿名函数(闭包)Go语言是支持函数式编程:

  1. 将匿名函数作为另一个函数的参数,回调函数。
  2. 将匿名函数作为另一个函数的返回值,可以形成闭包结构。

defer 语句

defer 关键字是 Go 语言中一个强大的工具,用于延迟执行函数调用,实现资源释放、错误处理和性能优化等功能。通过将需要延迟执行的操作放在 defer 语句中,可以保证这些操作会在函数返回前被执行,增加代码的可靠性和可维护性。

defer 在实际开发中经常被使用,例如:

  • 资源释放:defer 语句通常用于释放函数使用的各种资源,如文件、数据库连接、锁等。这样可以确保及时释放资源,避免内存泄漏或其他问题。
  • 错误处理:defer 可以用于统一处理函数调用过程中出现的错误。
  • 日志记录:defer 语句也可用于记录函数的执行时间,用于性能分析和调试。
  • 清理工作:defer 还可用于执行各种清理工作,比如关闭数据库连接、关闭窗口等。这对于防止资源泄漏很有帮助。

语法格式

go
func main() {
	// 这里的defer语句将在main函数返回前执行
    defer fmt.Println("World")
    fmt.Println("Hello")
}

示例

go
func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    // 使用bufio.Scanner读取文件内容
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    if err := scanner.Err(); err != nil {
        return err
    }
    return nil
}
go
func transferMoney(db *sql.DB, fromAccount, toAccount int, amount float64) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p) // 重新抛出panic
        } else if err != nil {
            tx.Rollback()
        } else {
            err = tx.Commit()
        }
    }()

    // 执行转账操作
    _, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromAccount)
    if err != nil {
        return err
    }
    _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toAccount)
    if err != nil {
        return err
    }

    return nil
}

指针

指针是一种数据类型,用于存储一个变量的内存地址的变量。指针变量的值是变量的地址,而不是变量的值。指针变量可以通过 & 运算符获取变量的地址,并通过 * 运算符获取指针指向的变量的值。

image-11

在上面图中,变量b的值为256,存在在内存地址为0xc00001a0e0。变量a的值为0xc00001a0e0,即变量a被认为指向变量b。

语法格式

go
var 指针变量名 *数据类型

示例

go
package main

import "fmt"

func main() {
	a := 10
	b := &a
	fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
	fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
	fmt.Println(&b)                    // 0xc00000e018
}

我们来看一下 b := &a 的图示:

image-12

指针的基本用法

获取变量的地址

在 Go 语言中可以通过 & 运算符获取变量的地址。

示例

go
package main

import "fmt"

func main() {
	// 通过 & 获取变量的内存地址
	s1 := "hello"
	fmt.Printf("s1的地址值:%p\n", &s1)
}

获取指针指向的变量的值

在 Go 语言中可以通过 * 运算符获取指针指向的变量的值。

示例

go
package main

import "fmt"

func main() {
	// 通过 * 获取指针指向的变量的值
	s1 := "hello"
	p1 := &s1
	fmt.Printf("p1指针指向的变量的值是:%s\n", *p1) // p1指针指向的变量的值是:hello
	// 修改指针变量的值
	*p1 = "world"
	fmt.Printf("s1的值是:%s\n", s1) // s1的值是:world
}

空指针

当一个指针被定义后没有分配任何变量时,这个指针的值为 nil 也就是空指针。

示例

go
var ptr *int
fmt.Println(ptr == nil) // 输出: true

使用new()函数创建指针

在Go语言中 new() 是一个内置的函数,用于动态地分配内存并返回指向该内存的指针。

语法格式

go
var 指针变量名 = new(数据类型)
go
指针变量名 := new(数据类型)

示例

go
// 使用new函数创建一个int类型的指针变量
ptr := new(int)
fmt.Printf("ptr的值是:%v\n", ptr)
fmt.Printf("ptr指向的变量的值是:%v\n", *ptr)
fmt.Printf("ptr自己的内存地址:%v\n", &ptr)

使用指针传递函数的参数

当需要在函数内部修改参数的值时,使用指针作为参数是一个很好的选择。使用指针可以直接修改原始数据,而不是只能修改副本。这在需要改变输入数据的场景中非常有用,比如更新数据结构、调整配置参数等。

使用指针作为函数参数的好处包括:

  • 可以修改原始数据。
  • 减少内存占用。
  • 支持空值。
  • 增加函数的灵活性。
  • 避免副作用。

示例

go
package main

import "fmt"

func main() {
	x, y := 10, 20
	fmt.Println("交换前 x =", x, "y =", y)
	swap(&x, &y)
	fmt.Println("交换后 x =", x, "y =", y)
}

func swap(a, b *int) {
	*a, *b = *b, *a
}
go
package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4, 5}
	fmt.Println("初始切片:", numbers)

	growSlice(&numbers, 3)
	fmt.Println("修改长度后切片:", numbers)
}

func growSlice(s *[]int, newLength int) {
	*s = (*s)[0:newLength]
}

多级指针

多级指针是指向指针的指针,可以有多个层级。在Go语言中,我们可以创建任意级别的指针。这个概念虽然不常用,但在某些特定场景下非常有用。

示例

go
// 定义一个int类型变量a
a := 10
fmt.Printf("a的值是%d,a的内存地址是:%p\n", a, &a) // a = 10
// 定义一个指针变量p1,指向变量a
p1 := &a
fmt.Printf("p1的数据类型是:%T,p1存储的值是:%v,p1的内存地址是:%p\n", p1, p1, &p1)
// 二级指针 这里存储的值是一级指针的内存地址 &p1
p2 := &p1
fmt.Printf("p2的数据类型是:%T,p2存储的值是:%v,p2的内存地址是:%p\n", p2, p2, &p2)
// 三级指针 这里存储的值是二级指针的内存地址 &p2
p3 := &p2
fmt.Printf("p3的数据类型是:%T,p3存储的值是:%v,p3的内存地址是:%p\n", p3, p3, &p3)
// 修改三级指针指向的变量存储的值
***p3 = 111
fmt.Printf("a的值是:%d\n", a) // a = 111

根据 MIT 许可证发布