07 go语言(golang) - 数据类型:指针 & 单元测试

指针

在Go语言中,指针是一种特殊的数据类型,它存储了另一个变量的内存地址。指针对于理解和使用计算机科学中的引用传递非常重要。通过使用指针,你可以直接访问和修改内存地址上的数据,这可以提高程序的效率并允许函数间直接修改传入变量的值。

基本概念

  • 声明指针 : 指针变量声明格式为 var ptr *T 其中 T 是指针所指向的值的类型。例如, var p *int 表示 p 是一个指向整数型数据的指针。
  • 获取变量地址 : 使用 & 运算符可以获取一个普通变量地址。
  • 使用指针访问值 : 使用解引用操作符 *, 可以获取或者设置位于该内存地址上(即该指针所"指向"的位置)的数据。
  • 指针的零值 : 指针的零值是 nil,表示指针不指向任何变量。
  • 空指针检查 : 在解引用指针之前,应该检查指针是否为 nil,以避免运行时错误。
go 复制代码
package main

import "fmt"

func main() {
	var a int = 100
	var p *int = &a // 定义一个整型类型的指针p,并将其初始化为a的地址
	//var p *int = nil // 也可以初始化为空

	fmt.Println("a的值:", a)
	fmt.Println("a的内存地址:", &a)
	fmt.Println("p的值,也就是a的内存地址:", p)
	fmt.Println("p的内存地址(指针的指针):", &p)

	fmt.Println("p的内存地址上保存的值(解引用p):", *p)

	*p = 200 // 更改*p意味着更改了p所执行到那个具体位置上保存着什么(即更改了a)
	fmt.Println("a的值:", a)
}

指针与函数

在Go语言中,所有参数都是按值传递(包括数组、结构体等)。如果需要在函数外部修改原始数据,则必须使用到Pointer。

go 复制代码
package main

import "fmt"

func modifyValue(p *int, a int) {
	*p = 10
	a = 20
}

func main() {
	x := 5
	y := 5
	modifyValue(&x, y)
	fmt.Println(x) // 输出10, 因为x 的值已经被modifyValue 函数修改了,modifyValue直接修改了指针p指向的值。
	fmt.Println(y) // 输出5, modifyValue函数中,a的值并没有被修改,因为a是函数的局部变量,函数结束后就自动释放了。
}
  1. 变量 x 被成功修改因为它是通过引用(或说地址/指针)传递给函数的。这允许直接访问和更改存储在该地址上的数据。
  2. 变量 y 没有被更改因为它是通过值传递给函数。即使在函数内部该参数被重新赋予新值(20),这种更改也仅限于函数作用域内,并未影响到原始变量。

指针与引用

Golang中的指针

特性

  • 可以直接操作内存,但不支持指针运算(如C/C++中的加减)。
  • 用于传递大型结构体时避免复制,提高效率。
go 复制代码
package main

import "fmt"

func main() {
    x := 10
    p := &x // 获取x的地址
    fmt.Println(*p) // 输出: 10

    *p = 20         // 修改p所指向的数据
    fmt.Println(x)  // 输出: 20
}

Java中的引用

Java没有显式的指针,但对象通过引用传递。

特性

  • 引用是对对象在堆内存中位置的一种间接访问方式。
  • 所有非基本类型(如对象、数组)都是通过引用来操作。
java 复制代码
public class Test {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.println(numbers[0]);
        modifyArray(numbers);
        System.out.println(numbers[0]); // 输出: 99,因为数组是通过引用传递并修改了原始数据。
    }

    public static void modifyArray(int[] arr) {
        arr[0] = 99;
    }
}

联系与对比

  • 内存管理:Go允许直接使用指针来管理内存,而Java则依赖垃圾回收机制自动管理对象生命周期,无需手动释放内存。
  • 安全性与复杂度:Go提供了更底层的控制能力,但也要求开发者更加小心地处理空指针问题。Java则隐藏了这些细节,提供更高层次抽象以提高安全性。

值传递

值传递(Pass by Value)是一种参数传递机制,其中函数接收的是实参的副本,而不是实参本身。这意味着在函数内部对参数进行的任何修改都不会影响到原始变量。每次调用函数时,都会为参数创建一个新的内存空间来存储这些副本。

特性

  1. 独立性:由于函数接收到的是实参的副本,因此在函数内部对该参数所做的任何更改都不会影响到外部变量。

  2. 安全性:这种机制确保了调用者和被调用者之间的数据隔离,避免了不小心修改原始数据的问题。

  3. 适用场景:值传递通常用于基本数据类型(如整数、浮点数、布尔值等),因为它们占用内存较少且复制开销低。

示例

go 复制代码
package main

import "fmt"

func modifyValue(val int) {
    val = 20 // 修改val,但这只是在modifyValue作用域内有效
}

func main() {
    x := 10
    fmt.Println("Before modifyValue:", x) // 输出: Before modifyValue: 10
    modifyValue(x)
    fmt.Println("After modifyValue:", x)  // 输出: After modifyValue: 10
}
  • x是一个整数,当我们将其传递给modifyValue时,实际上是将x的当前值(即10)的副本赋给了形参val
  • modifyValue中,将形参设置为20并不会改变主程序中变量x的值,因为它们位于不同的内存空间。
  • 因此,在执行完函数后,输出仍然是初始值10,这证明了Go使用的是值传递机制。

特殊

然而,对于切片、映射等引用类型的数据结构,传递的值实际上是一个包含指向底层数据的指针的结构。因此,即使是值传递,也能通过这个指针修改底层数据。

go 复制代码
func main() {
	numbers := []int{1, 2, 3}
	fmt.Println(numbers[0])
	modifyArray(numbers)
	fmt.Println(numbers[0]) // 输出: 99,因为切片是通过引用传递并修改了原始数据。
}

func modifyArray(arr []int) {
	arr[0] = 99
}
  • 切片结构 :切片本质上是一个描述符,包含三个部分:
    • 指向底层数组的指针。
    • 切片长度。
    • 切片容量。
  • 值传递:当你将切片作为参数传入函数时,复制的是这个描述符(即这三个部分),而不是整个底层数组。

练习

  1. 定义一个结构体Person,包含字段Name(字符串类型)和Age(整数类型)。关于结构体我们后续详细展开学
  2. 实现一个函数试图通过值传递来更新结构体的名称。
  3. 实现另一个函数试图通过指针传递来更新结构体的年龄。

实现

go 复制代码
package main

import "fmt"

func main() {
	var p Person = Person{"小明", 80}
	fmt.Println(p)

	// 通过值传递修改名字,不会修改原来的值
	updateName(p, "大明")
	fmt.Println(p)

	updateAge(&p, 90)
	fmt.Println(p)
}

func updateAge(ptr *Person, newAge int) {
	// 当你有一个指向结构体的指针ptr时,Go允许你使用相同的.运算符来访问字段,如ptr.Age。这实际上是对 (*ptr).Age 的简写。
	// 编译器会自动解引用这个指针以获取实际的值,然后再进行字段访问。
	ptr.Age = newAge
}

func updateName(p Person, newName string) {
	p.Name = newName
}

type Person struct {
	Name string
	Age  int
}
  • 注意在使用值传递时,对象本身不会被改变,而是对其副本进行操作。
  • 使用指针可以直接修改对象,因为它们允许直接访问内存地址。

单元测试

在模块中只允许一个main方法,调试代码极其不方便 ,所以这里引入单元测试,让代码调试方便许多。

单元测试是通过标准库中的testing包来实现的。Go提供了一种简单而强大的机制来编写和运行测试。

编写单元测试

  1. 创建测试文件

    • 测试文件通常与被测代码放在同一目录下,并以 _test.go 结尾。例如 math_test.go
  2. 编写测试函数

    • 测试函数必须以 Test 开头,并且接收一个指向 testing.T 类型的参数。
    • 函数签名示例: func TestFunctionName(t *testing.T)
  3. 使用断言

    • Go没有内置断言库,但可以使用条件语句和t.Errorf()t.Fatalf()来报告错误。
  4. 示例代码

go 复制代码
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {

	println("单元测试")

	// 打印错误日志
	t.Log("日志!")

	t.Errorf("错误!!!!")

	t.Fatalf("严重错误!!!!")

}

运行单元测试

  • 使用命令行工具:在终端中导航到包含 _test.go 文件的目录,然后执行以下命令:
bash 复制代码
go test
  • 如果需要查看详细信息,可以使用 -v 标志:
bash 复制代码
go test -v
  • 或者在idea中直接右键执行run

输出

复制代码
=== RUN   Test1
单元测试
    math_test.go:11: 日志!
    math_test.go:13: 错误!!!!
    math_test.go:15: 严重错误!!!!
--- FAIL: Test1 (0.00s)

FAIL

Process finished with the exit code 1
相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio