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
相关推荐
Kisorge30 分钟前
【C语言】指针数组、数组指针、函数指针、指针函数、函数指针数组、回调函数
c语言·开发语言
轻口味2 小时前
命名空间与模块化概述
开发语言·前端·javascript
晓纪同学2 小时前
QT-简单视觉框架代码
开发语言·qt
威桑2 小时前
Qt SizePolicy详解:minimum 与 minimumExpanding 的区别
开发语言·qt·扩张策略
飞飞-躺着更舒服3 小时前
【QT】实现电子飞行显示器(简易版)
开发语言·qt
明月看潮生3 小时前
青少年编程与数学 02-004 Go语言Web编程 16课题、并发编程
开发语言·青少年编程·并发编程·编程与数学·goweb
明月看潮生3 小时前
青少年编程与数学 02-004 Go语言Web编程 17课题、静态文件
开发语言·青少年编程·编程与数学·goweb
Java Fans3 小时前
C# 中串口读取问题及解决方案
开发语言·c#
盛派网络小助手3 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
007php0073 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程