Go基础之函数和方法讲解

1 自定义函数

1.1 函数定义

Go 语言函数定义格式如下:

func function_name( [parameter list] ) [return_types] {
   函数体
}

函数定义解析:

  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。

1.2 函数调用与返回多值

1.2.1 返回类型

Go 函数可以返回多个值,例如:

package main

import "fmt"

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b)
}
以上实例执行结果为:
Runoob Google

1.2.2 命名返回值

在 Go 中,函数返回值可以同时指定类型返回参数的名字。这种方式叫做 命名返回值,它提供了一种方便的方法来明确返回值的含义,并可以在函数体中直接操作这些命名返回值,无需显式声明和返回它们。
如果指定了返回值的具体名字,则可以认为是在函数体的第一行声明了该名字的变量

在函数定义时,如果返回值不仅指定类型,还给出了名字,Go 会为这些返回值在函数体内隐式声明变量。函数结束时,返回的值会自动从这些命名变量中获取

注意:所有定义了返回值的函数必须显式地使用 return 语句返回值,即使返回值是命名的,即:虽然在命名返回值的函数中return语句不会显示的返回任何值,但是不可以省略 return关键字

func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
        dData := DivideError{
            dividee: varDividee,
            divider: varDivider,
        }
        errorMsg = dData.Error() // 直接操作返回值 `errorMsg`
        return
    } else {
        return varDividee / varDivider, "" // 直接赋值给命名返回值 `result`    
    }
}

命名返回值的作用:

  • 隐式声明变量: 在函数体中,可以直接使用 result 和 errorMsg 变量,而无需手动声明
  • 自动返回: 使用 return 语句时,如果没有指定具体的返回值,Go 会返回当前命名返回变量的值。

对比未命名返回值,主要区别:

  • 未命名返回值需要在 return 时显式列出所有返回值。
  • 命名返回值提供了更清晰的语义和更简洁的 return 语句。

注意

  • 如果在函数中使用了命名返回值,但未对其赋值,Go 会自动为命名返回值提供 零值
  • 命名返回值过多时,可能会降低代码的简洁性和可读性,因此适度使用。
  • 命名返回值主要适用于返回值数量少、意义明确的场景。

1.3 函数参数

1.3.1 值传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200

   fmt.Printf("交换前 a 的值为 : %d\n", a )
   fmt.Printf("交换前 b 的值为 : %d\n", b )

   /* 通过调用函数来交换值 */
   swap(a, b)

   fmt.Printf("交换后 a 的值 : %d\n", a )
   fmt.Printf("交换后 b 的值 : %d\n", b )
}

/* 定义相互交换值的函数 */
func swap(x, y int) int {
   var temp int

   temp = x /* 保存 x 的值 */
   x = y    /* 将 y 值赋给 x */
   y = temp /* 将 temp 值赋给 y*/

   return temp;
}
结果:
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200

1.3.2 引用传递

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int= 200

   fmt.Printf("交换前,a 的值 : %d\n", a )
   fmt.Printf("交换前,b 的值 : %d\n", b )

   /* 调用 swap() 函数
   * &a 指向 a 指针,a 变量的地址
   * &b 指向 b 指针,b 变量的地址
   */
   swap(&a, &b)

   fmt.Printf("交换后,a 的值 : %d\n", a )
   fmt.Printf("交换后,b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

结果:
交换前,a 的值 : 100
交换前,b 的值 : 200
交换后,a 的值 : 200
交换后,b 的值 : 100

1.3.3 不定参数

可以通过在参数类型前加上省略号(...)来定义不定参数。不定参数在函数内部表现为一个切片

package main
import "fmt"

func sum(numbers ...int) int {
    total := 0
    for _, number := range numbers {
        total += number
    }
    return total
}
func main() {
    fmt.Println(sum(1, 2, 3))  // 输出: 6
    fmt.Println(sum(4, 5, 6, 7))  // 输出: 22
}

注意:不能把切片作为可变参数传递给函数,如下将报错:cannot use nums (type []int) as type int in argument to find

package main

import (
	"fmt"
)

func find(num int, nums ...int) {     
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i, v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}
	fmt.Printf("\n")
}
func main() {    
	nums := []int{89, 90, 95}
	find(89, nums)
}

nums作为一个可变参数传递给find函数。这些可变参数将转换为int类型的切片。在这种情况下,nums已经是一个int类型切片,编译器尝试使用nums创建一个新的[]int切片,像这样:find(89, []int{ nums}),因为nums 是一个 []int 类型 而不是 int类型。类型根本就不相同,所以会失败。
那么有没有办法将切片传递给变参函数?答案是肯定的。
有一个语法糖可用于将切片传递给变参函数。必须为切片添加后缀...,把切片展开,这样则可以将切片直接传递给函数,而不会创建新切片。

在上面的程序中,如果将第 find(89, nums)替换为 find(89, nums...),程序将成功编译

1.4 函数变量&回调

1.4.1 函数变量

Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。以下实例中在定义的函数中初始化一个变量,该函数仅仅是为了使用内置函数 math.sqrt(),实例为:

package main

import (
   "fmt"
   "math"
)

func main(){
   /* 声明函数变量 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }

   /* 使用函数 */
   fmt.Println(getSquareRoot(9))

}

1.4.2 函数回调

package main
import "fmt"

// 声明一个函数类型
type cb func(int) int

func main() {
    testCallBack(1, callBack)
    testCallBack(2, func(x int) int {
        fmt.Printf("我是回调,x:%d\n", x)
        return x
    })
}

func testCallBack(x int, f cb) {
    f(x)
}

func callBack(x int) int {
    fmt.Printf("我是回调,x:%d\n", x)
    return x
}

1.5 匿名函数

匿名函数,可作为闭包。匿名函数是一个内联语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
匿名函数是一种没有函数名的函数,通常用于在函数内部定义函数,或者作为函数参数进行传递。

package main

import "fmt"
func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
     return i  
   }
}

func main(){
   /* nextNumber 为一个函数,函数 i 为 0 */
   nextNumber := getSequence()  

   /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   
   /* 创建新的函数 nextNumber1,并查看结果 */
   nextNumber1 := getSequence()  
   fmt.Println(nextNumber1())
   fmt.Println(nextNumber1())
}

带参数的匿名函数

package main
import "fmt"
func main() {
    add_func := add(1,2)
    fmt.Println(add_func(1,1))
    fmt.Println(add_func(0,0))
    fmt.Println(add_func(2,2))
} 
// 闭包使用方法
func add(x1, x2 int) func(x3 int,x4 int)(int,int,int)  {
    i := 0
    return func(x3 int,x4 int) (int,int,int){ 
       i++
       return i,x1+x2,x3+x4
    }
}

1.6 defer函数

1.6.1 定义

defer 是 Go 中的一个关键字,用于延迟函数调用,它会将指定的函数推迟到包含它的函数返回时才执行。无论函数是正常返回还是遇到 panic 退出,defer 注册的调用都会执行,因此常用于资源释放清理操作等场景。

执行顺序:defer 的调用按照先进后出LIFO)顺序执行。

核心特点,延迟调用:

  • defer 会在包含它的函数返回时才执行,即使函数中有多个 return。
  • 遇到 panic(抛出异常),defer 的调用也会在 panic 传播前执行。

defer 语句的执行时机:

  • defer 语句会在 函数执行完所有代码 后,但在 函数返回值发送给调用者之前 执行。
  • 换句话说,defer 语句是在函数的 return 语句执行后、实际返回函数值给调用者之前执行的。

defer 中的参数求值:

  • defer 中的参数会在 defer 声明时 立即求值,而不是等到 defer 执行时才求值。

1.6.2 使用方式

func main() {
    fmt.Println("Start")
    defer fmt.Println("Defer 1")
    defer fmt.Println("Defer 2")
    fmt.Println("End")
}

输出:
Start
End
Defer 2
Defer 1
mathematica

1.6.3 参数求值

defer 调用的函数,其参数在 defer 声明时就会被求值,而不是等到执行时才求值。
示例:

func main() {
    x := 5
    defer fmt.Println("x =", x)
    x = 10
}
输出:

x = 5

defer 注册的是 fmt.Println("x =", 5)。

Go 中的 defer 是在函数的 return 语句执行后,但在实际返回值发送给调用者之前执行的。然而,defer 的参数在声明时就会被计算,而不是等到 defer 执行时才计算。这个特性会影响返回值的行为,特别是在 defer 中对参数做修改时

package main

import "fmt"

func test() int {
    a := 5
    defer func() int {
        a++  // 修改 a 的值 ,但此处a 还是5 ,所以下面Defer是6
        fmt.Println("Defer:", a)
        return a  // 这个 return 语句不会影响 test() 函数的返回值
    }()
    return a++  // 返回 a 的当前值(5),然后 a 自增
}

func main() {
    fmt.Println("Return Value:", test())  // 输出返回值
}
结果:
Defer: 6
Return Value: 5

但是 命名返回函数会修改

package main

import "fmt"

func test() (result int) {
    result = 5
    defer func() {
        result++  // 修改 result 的值
        fmt.Println("Defer result:", result)
    }()
    return result  // 返回 result(初始值是 5)
}

func main() {
    fmt.Println("Return Value:", test())  // 输出返回值
}

1.6.4 LIFO 执行顺序

多个 defer 按照后注册的先执行。

1.6.4.1 资源清理

用于关闭文件、解锁互斥锁、关闭数据库连接等。
示例:

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close() // 确保函数结束时关闭文件

    // 读取文件内容
    buf := make([]byte, 100)
    file.Read(buf)
    fmt.Println(string(buf))
}

1.6.4.2 互斥锁解锁

var mu sync.Mutex

func criticalSection() {
    mu.Lock()
    defer mu.Unlock() // 保证函数结束时解锁

    // 临界区代码
    fmt.Println("In critical section")
}

1.6.4.3 捕获 panic 并恢复

使用 defer 和 recover 捕获和处理 panic,防止程序崩溃。

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    fmt.Println("Start")
    panic("Something went wrong!") // 触发 panic
    fmt.Println("End") // 不会被执行
}
输出:
Start
Recovered from panic: Something went wrong!

1.6.4.4 函数退出日志

func trace(s string) {
    fmt.Println("Enter:", s)
    defer fmt.Println("Exit:", s)
}

func main() {
    trace("main")
    fmt.Println("Doing work in main")
}
输出:
Enter: main
Doing work in main
Exit: main

1.7 方法

方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:

func (variable_name variable_data_type) function_name() [return_type]{
   /* 函数体*/
}

下面定义一个结构体类型和该类型的一个方法:

package main

import (
   "fmt"  
)

/* 定义结构体 */
type Circle struct {
  radius float64
}

func main() {
  var c1 Circle
  c1.radius = 10.00
  fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
  //c.radius 即为 Circle 类型对象中的属性
  return 3.14 * c.radius * c.radius
}

// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64)  {
   c.radius = radius
}

方法的接受者可以是命名类型结构体类型下的值或指针,一共四种情况:

package main

import (
    "fmt"
    "math"
)

// 定义圆形结构体
type Circle struct {
    Radius float64
}

// 定义命名类型
type MyFloat float64

// 使用结构体值作为接收者
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// 使用结构体指针作为接收者
func (c *Circle) AreaPointer() float64 {
    return math.Pi * c.Radius * c.Radius
}

// 使用命名类型值作为接收者
func (f MyFloat) Square() float64 {
    return f * f
}

// 使用命名类型指针作为接收者
func (f *MyFloat) Cube() float64 {
    return *f * *f * *f
}

func main() {
    var c Circle
    c.Radius = 10.00

    // 调用圆形的Area方法
    fmt.Println("Area of circle:", c.Area())
    // 调用圆形的AreaPointer方法
    fmt.Println("Area of circle (pointer):", c.AreaPointer())

    var f MyFloat = 2.0
    // 调用MyFloat的Square方法
    fmt.Println("Square of MyFloat:", f.Square())
    // 调用MyFloat的Cube方法
    fmt.Println("Cube of MyFloat:", f.Cube())
}

传递指针时自动转,一个指针接收器的方法既可以接受值接收器 也可以接受指针接收器,一个值接收器的方法既可以接受值接收器 也可以接受指针接收器

// 传递指针
func (s *Student) chushi(name string, age int) {     
	s.Name = name
	s.Age = age
}
func main() {        
	var stu Student = Student{        
		Name: "lisi",
		Age: 20,
	}
    // (&stu).chushi("zhangsan",18)	指针自动转化
	stu.chushi("zhangsan",18)
	fmt.Println(stu)
}

2 内置函数

2.1 初始化函数

在 Go 中,每个 .go 文件 都可以有一个 init 函数,并且一个文件中可以定义多个 init函数,但是它只被初始化一次。这些 init 函数有特定的用途和执行顺序。

2.1.1 简介

init 函数的特点:

  • 无参数、无返回值:
func init() {
    // 初始化代码
}
  • 自动执行:
    init 函数会在程序运行时自动执行,无需显式调用。
  • 初始化逻辑:
    常用于初始化包级变量、设置运行环境、或者执行一些启动时的准备工作。
  • 一个文件可以有多个 init 函数:
    同一个文件中可以定义多个 init 函数,甚至分布在不同的 .go 文件中。

init 函数的执行顺序:

  • 按包的依赖顺序执行:
    如果包 A 依赖包 B,则会先初始化包 B,再初始化包 A。
  • 同一包中按文件顺序执行:
    Go 编译器会按照文件名的字典顺序,逐个初始化每个文件。
  • 同一文件中按代码顺序执行:
    在一个文件中,多个 init 函数会按代码中出现的顺序依次执行。
  • main 包的特殊性:
    程序总是先执行所有依赖包的 init 函数,然后再执行 main 包的 init,最后是 main 函数。

2.1.2 示例

包中多个文件的 init 执行顺序

mypackage/
  |- file1.go
  |- file2.go
main.go

file1.go:

package mypackage

import "fmt"

func init() {
    fmt.Println("Init in file1")
}

func File1Func() {
    fmt.Println("Function in file1")
}

file2.go:

package mypackage

import "fmt"

func init() {
    fmt.Println("Init in file2")
}

func File2Func() {
    fmt.Println("Function in file2")
}

main.go:

package main

import (
    "fmt"
    "mypackage"
)

func init() {
    fmt.Println("Init in main")
}

func main() {
    fmt.Println("Main function")
    mypackage.File1Func()
    mypackage.File2Func()
}

它通常用于初始化包级变量、依赖设置和其他启动前的准备工作。

2.2 输入输出函数

2.2.1 输入函数

输入函数:

  • Scan函数:
    Scan 从标准输入中读取数据,并将数据用空白分割并解析后存入 a 提供的变量中(换行符会被当作空白处理),变量必须以指针传入。
    当读到 EOF 或所有变量都填写完毕则停止扫描。返回成功解析的参数数量。
    func Scan(a ...interface{}) (n int, err error)
  • Scanln函数:
    ScanlnScan 类似,只不过遇到换行符就停止扫描。
    func Scanln(a ...interface{}) (n int, err error)
  • Scanf函数:
    Scanf标准输入中读取数据,,返回成功解析的参数数量,并根据格式字符串 format 对数据进行解析,将解析结果存入参数 a 所提供的变量中,变量必须以指针传入。
    输入端的换行符必须和 format 中的换行符相对应(如果格式字符串中有换行符,则输入端必须输入相应的换行符)。
    占位符 %c 总是匹配下一个字符,包括空白,比如空格符、制表符、换行符。
    func Scanf(format string, a ...interface{}) (n int, err error)

其他读入函数:

  • 从io 中读取数据:
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
  • 从 str 中读取数据。
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

示例:

// 对于 Scan 而言,回车视为空白
func main() {
    a, b, c := "", 0, false
    fmt.Scan(&a, &b, &c)
    fmt.Println(a, b, c)
    // 在终端执行后,输入 abc 1 回车 true 回车
    // 结果 abc 1 true
}
// 对于 Scanln 而言,回车结束扫描
func main() {
    a, b, c := "", 0, false
    fmt.Scanln(&a, &b, &c)
    fmt.Println(a, b, c)
    // 在终端执行后,输入 abc 1 true 回车
    // 结果 abc 1 true
}
// 格式字符串可以指定宽度
func main() {
    a, b, c := "", 0, false
    fmt.Scanf("%4s%d%t", &a, &b, &c)
    fmt.Println(a, b, c)
    // 在终端执行后,输入 1234567true 回车
    // 结果 1234 567 true
}

2.2.2 输出函数

2.2.2.1 常见输出

  • Print 函数
    将参数列表 a 中的各个参数转换为字符串并写入到标准输出中。
    非字符串参数之间会添加空格,返回写入的字节数。
    func Print(a ...interface{}) (n int, err error)
  • Println 函数
    类似 Print,只不过最后会添加一个换行符。
    所有参数之间会添加空格,返回写入的字节数。
    func Println(a ...interface{}) (n int, err error)
  • Printf函数
    将参数列表 a 填写到格式字符串 format 的占位符中。
    填写后的结果写入到标准输出中,返回写入的字节数。
    func Printf(format string, a ...interface{}) (n int, err error)

其他输出函数:

  • io 中输出数据:
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
  • 从 str 中输出数据:
func Sprint(a ...interface{}) string
func Sprintln(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string

2.2.2.2 Printf函数格式

Printf 字符串格式化符号:

格 式 描 述
%v 按值的本来值输出
%+v 在 %v 基础上,对结构体字段名和值进行展开
%#v 输出 Go 语言语法格式的值
%T 输出 Go 语言语法格式的类型和值
%% 输出 % 本体
%b 整型以二进制方式显示
%o 整型以八进制方式显示
%d 整型以十进制方式显示
%x 整型以十六进制方式显示
%X 整型以十六进制、字母大写方式显示
%U Unicode 字符
%f 浮点数
%p 指针,十六进制方式显示

对齐方式,通过在格式化字符串中使用宽度和对齐参数,可以控制生成的字符串的对齐方式。
常用的对齐参数有:

  • %s:字符串格式,可以使用以下对齐参数:
    • %s:默认对齐方式,左对齐。
    • %10s:指定宽度为 10 的右对齐。
    • %-10s:指定宽度为 10 的左对齐。
  • %d:整数格式,可以使用以下对齐参数:
    • %d:默认对齐方式,右对齐。
    • %10d:指定宽度为 10 的右对齐。
    • %-10d:指定宽度为 10 的左对齐。
  • %f:浮点数格式,可以使用以下对齐参数:
    • %f:默认对齐方式,右对齐。
    • %10f:指定宽度为 10 的右对齐。
    • %-10f:指定宽度为 10 的左对齐。

示例

package main

import (
    "fmt"
    "os"
)

type point struct {
    x, y int
}

func main() {

    p := point{1, 2}
    fmt.Printf("%v\n", p)

    fmt.Printf("%+v\n", p)

    fmt.Printf("%#v\n", p)

    fmt.Printf("%T\n", p)

    fmt.Printf("%t\n", true)

    fmt.Printf("%d\n", 123)

    fmt.Printf("%b\n", 14)

    fmt.Printf("%c\n", 33)

    fmt.Printf("%x\n", 456)

    fmt.Printf("%f\n", 78.9)

    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)

    fmt.Printf("%s\n", "\"string\"")

    fmt.Printf("%q\n", "\"string\"")

    fmt.Printf("%x\n", "hex this")

    fmt.Printf("%p\n", &p)

    fmt.Printf("|%6d|%6d|\n", 12, 345)

    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)

    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)

    fmt.Printf("|%6s|%6s|\n", "foo", "b")

    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")

    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)

    fmt.Fprintf(os.Stderr, "an %s\n", "error")
}

输出结果为:
{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
true
123
1110
!
1c8
78.900000
1.234000e+08
1.234000E+08
"string"
"\"string\""
6865782074686973
0xc0000b4010
|    12|   345|
|  1.20|  3.45|
|1.20  |3.45  |
|   foo|     b|
|foo   |b     |
a string
an error

2.3 测试函数

在 Go 语言中,测试函数(Test Functions)是通过 Go 的内置测试框架 testing 来进行单元测试和集成测试的。Go 提供了一些简洁的工具来创建、运行和管理测试,以下是对 Go 测试函数的详细讲解:

2.3.1 测试文件结构

Go 测试文件通常以 _test.go 作为后缀名,并且测试函数必须放在相同的包(package)内。

假设有一个 Go 文件 math.go,它包含一个简单的加法函数:

package math

func Add(a, b int) int {
    return a + b
}

可以创建一个测试文件 math_test.go 来测试 Add 函数:

package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Errorf("Add(1, 2) = %d; want 3", result)
    }
}

2.3.2 测试函数的命名

测试函数的命名必须以 Test 开头,后面跟着要测试的功能名称或方法名称。测试函数的签名必须是:func TestXxx(t *testing.T)
其中 Xxx 是测试的功能或方法名。

func TestAdd(t *testing.T) {
    // 测试代码
}

2.3.3 testing.T 类型

Test 函数接收一个指向 testing.T 类型的指针。T 类型提供了多个方法来帮助我们报告错误和测试结果:

  • t.Errorf(format string, args ...):如果测试失败,使用这个方法报告错误,格式和 fmt.Printf 相似。
  • t.Fatal(args ...):如果测试失败,停止执行并报告错误。
  • t.Log(args ...):在测试中打印日志信息,默认情况下日志不会显示,只有在使用 -v 标志时才会显示。
  • t.Skip(args ...):跳过当前测试。

示例:

func TestAdd(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Errorf("Add(1, 2) = %d; want 3", result)
    }
}

2.3.4 运行测试

要运行测试,可以使用 go test 命令。Go 会自动查找所有以 _test.go 结尾的文件,并运行其中的所有测试函数。
go test 会自动寻找当前目录下所有以 Test 开头的函数并运行。
默认情况下,测试会在无输出模式下运行,只有失败时才显示错误信息。
其他常用的 go test 命令
go test -v:显示详细输出,包括每个测试函数的执行情况。
go test -run TestAdd:只运行名为 TestAdd 的测试函数。
go test -bench .:运行基准测试(benchmark)。
go test -cover:报告代码覆盖率。

2.3.5 基准测试(Benchmark Tests)

Go 也支持基准测试,用于测量代码性能。基准测试函数的命名规则是 BenchmarkXxx,例如:

package math

import "testing"

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
    // 如果在这里做测试用的数据的生成,不想把这个时间加进去,可以在生成数据之后reset一下时间
	b.ResetTimer()
}

其中的 b.N: 运行的次数,Go 会自动决定 b.N 的值,以便进行足够多次的性能测量。
要运行基准测试:

go test -bench .

2.3.6 命令行测试

  • go test -coverprofile=c.out 生成文件
  • go tool cover -html=c.out 页面观看覆盖率
    注意test文件要和逻辑代码在同一个包中 不然显示不出来
  • go test -bench . -cpuprofile=cpu.out 生成二进制文件
    pprof查看这个二进制文件go tool pprof cpu.out 进入pprof环境
  • go语言的文档生成用 go doc 变量
    在页面中看:go doc -http :8080
posted @ 2024-12-29 08:25  上善若泪  阅读(74)  评论(0编辑  收藏  举报