指针
在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是函数的局部变量,函数结束后就自动释放了。
}
- 变量 x 被成功修改因为它是通过引用(或说地址/指针)传递给函数的。这允许直接访问和更改存储在该地址上的数据。
- 变量 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)是一种参数传递机制,其中函数接收的是实参的副本,而不是实参本身。这意味着在函数内部对参数进行的任何修改都不会影响到原始变量。每次调用函数时,都会为参数创建一个新的内存空间来存储这些副本。
特性
-
独立性:由于函数接收到的是实参的副本,因此在函数内部对该参数所做的任何更改都不会影响到外部变量。
-
安全性:这种机制确保了调用者和被调用者之间的数据隔离,避免了不小心修改原始数据的问题。
-
适用场景:值传递通常用于基本数据类型(如整数、浮点数、布尔值等),因为它们占用内存较少且复制开销低。
示例
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
}
- 切片结构 :切片本质上是一个描述符,包含三个部分:
- 指向底层数组的指针。
- 切片长度。
- 切片容量。
- 值传递:当你将切片作为参数传入函数时,复制的是这个描述符(即这三个部分),而不是整个底层数组。
练习
- 定义一个结构体
Person
,包含字段Name
(字符串类型)和Age
(整数类型)。关于结构体我们后续详细展开学 - 实现一个函数试图通过值传递来更新结构体的名称。
- 实现另一个函数试图通过指针传递来更新结构体的年龄。
实现
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提供了一种简单而强大的机制来编写和运行测试。
编写单元测试
-
创建测试文件:
- 测试文件通常与被测代码放在同一目录下,并以
_test.go
结尾。例如math_test.go
。
- 测试文件通常与被测代码放在同一目录下,并以
-
编写测试函数:
- 测试函数必须以
Test
开头,并且接收一个指向testing.T
类型的参数。 - 函数签名示例:
func TestFunctionName(t *testing.T)
- 测试函数必须以
-
使用断言:
- Go没有内置断言库,但可以使用条件语句和
t.Errorf()
或t.Fatalf()
来报告错误。
- Go没有内置断言库,但可以使用条件语句和
-
示例代码
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