Go语言特性
- Go函数只有 值传递
- Go指针 不能运算
- 指针类型的定义与基础数据类型有关,即指针类型是与其指向的变量的类型相关联的。
Go语言指针
使用new函数: new函数用于分配内存,并返回一个指向该类型的新零值的指针。
- new(int)分配了一个int类型的内存,并返回一个指向该内存的指针。
package main
import "fmt"
func main() {
var p *int
p = new(int)
fmt.Println(*p, p, &p) // 输出: 0 0xc00009e018 0xc0000a4018
}
取地址符号(&): 取地址符号&可以获取一个变量的内存地址。
- &a获取变量a的内存地址,并将其赋值给指针变量p。
package main
import "fmt"
func main() {
var a int = 42
var p *int = &a
fmt.Println(*p, p, &p) // 输出: 42 0xc000112008 0xc000108018
}
- *p 是指针p所指向的变量的值,即a的值,输出42。
- p 是指针变量,存储的是变量a的内存地址,这个地址可能是像 0xc000112008 这样的值(实际地址在每次运行时可能会不同)。
- &p 是指针变量p本身的内存地址,因为p是定义在函数栈上的一个变量,所以它也有自己的内存地址,类似 0xc000108018 这样的值(实际地址在每次运行时可能会不同)。
使用*操作符: 指针也可以直接通过*操作符进行解引用或指向具体的值。
- *p = 42将值42赋值给指针p所指向的内存地址。
package main
import "fmt"
func main() {
var p *int
p = new(int)
*p = 42
fmt.Println(p, *p, &p) // 输出:0xc00009e018 42 0xc0000a4018
}
作为函数参数: 函数参数也可以是指针类型,允许在函数内修改变量的值。Go语言函数只有值传递,所以常用指针作为函数参数。
package main
import "fmt"
func setToZero(p *int) {
*p = 0
}
func main() {
var x int = 5
setToZero(&x)
// x 现在是 0
fmt.Println(x) // 输出:0
}
指向结构体的指针: 结构体(struct)是用户定义的类型,用于将一组数据组合在一起。指向结构体的指针在处理复杂数据和需要修改结构体字段的场景中非常有用。
package main
import "fmt"
func main() {
type Person struct {
Name string
Age int
}
var person Person = Person{Name: "Alice", Age: 30}
var pPerson *Person = &person
fmt.Println(pPerson) // 输出: &{Alice 30}
fmt.Println(*pPerson) // 输出: {Alice 30}
fmt.Println(&pPerson) // 输出: 0xc000054020
}
作为函数返回值: 返回指针可以用于创建和返回函数内部的变量,或者用于修改调用者传入的数据。使用指针能够提高性能,避免不必要的值拷贝,但需要注意避免悬空指针和内存泄漏等问题。
package main
import "fmt"
// 定义一个返回指针的函数
func createIntPointer(value int) *int {
// 在函数内部创建一个变量
v := value
// 返回该变量的指针
return &v
}
func main() {
// 调用返回指针的函数
p := createIntPointer(42)
// 打印指针和值
fmt.Println("指针:", p) // 输出: 指针: 0xc0000121a8
fmt.Println("值:", *p) // 输出: 值: 42
}
- 返回结构体指针是一种常见的实践,尤其是在需要创建新的结构体实例并将其返回给调用者时。这样可以避免结构体的值拷贝,直接操作内存中的数据,提高效率。
package main
import "fmt"
// 定义一个结构体类型
type Person struct {
Name string
Age int
}
// 定义一个返回结构体指针的函数
func NewPerson(name string, age int) *Person {
// 创建结构体实例
person := Person{
Name: name,
Age: age,
}
// 返回结构体的指针
return &person
}
func main() {
// 调用返回结构体指针的函数
p := NewPerson("Alice", 30)
// 访问和修改结构体字段
fmt.Println("Name:", p.Name) // 输出: Name: Alice
fmt.Println("Age:", p.Age) // 输出: Age: 30
// 修改结构体字段
p.Age = 31
fmt.Println("Updated Age:", p.Age) // 输出: Updated Age: 31
}
结构体中的指针字段: 结构体字段可以是指针类型,这使得我们可以更灵活地操作复杂的数据结构。
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func main() {
node1 := &Node{Value: 1}
node2 := &Node{Value: 2}
node1.Next = node2
fmt.Println(node1) // 输出: &{1 0xc000014080}
fmt.Println(node1.Next) // 输出: &{2 <nil>}
}
指向数组的指针: 数组的指针可以通过指向数组的第一个元素来获得。
package main
import "fmt"
func main() {
var arr [3]int = [3]int{1, 2, 3}
var p *[3]int = &arr
fmt.Println(p) // 输出: &[1 2 3]
fmt.Println(*p) // 输出: [1 2 3]
fmt.Println(&p) // 输出: 0xc000054020
}
指针数组: 指针数组是一个存储指针的数组,每个元素都是一个指向某类型的指针。
package main
import "fmt"
func main() {
var arr [3]*int
a, b, c := 1, 2, 3
arr[0] = &a
arr[1] = &b
arr[2] = &c
fmt.Println(arr) // 输出: [0xc00009e018 0xc00009e020 0xc00009e028]
}
指向切片的指针: 切片(slice)是一种动态数组,可以在程序运行时动态调整大小。虽然切片本身已经是一个引用类型,通常情况下不需要使用指向切片的指针,但在某些特殊场景下,指向切片的指针可以更高效或更简洁地处理一些操作。
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
// 定义一个指向切片的指针
var p *[]int = &slice
// 使用指向切片的指针修改切片
*p = append(*p, 4, 5, 6)
// 输出切片内容
fmt.Println(slice) // 输出: [1 2 3 4 5 6]
fmt.Println(p, *p, &p) // 输出: &[1 2 3 4 5 6] [1 2 3 4 5 6] 0xc000054020
}
指向映射的指针: 映射(map)是一种内置的数据结构,用于存储键值对。与切片类似,映射本质上是引用类型,因此在大多数情况下,直接传递映射就足够了。然而,在某些特定的场景下,使用指向映射的指针(map pointer)可以提供更高的灵活性和性能优化。
package main
import "fmt"
func main() {
// 定义一个映射
m := map[string]int{"one": 1, "two": 2}
// 定义一个指向映射的指针
var p *map[string]int = &m
// 使用指向映射的指针修改映射
(*p)["three"] = 3
// 输出映射内容
fmt.Println(m) // 输出: map[one:1 three:3 two:2]
fmt.Println(p, *p, &p) // 输出: &map[one:1 three:3 two:2] map[one:1 three:3 two:2] 0xc000054028
}
指向函数的指针: 函数也是一种类型,因此可以创建指向函数的指针。这种功能在实现回调机制、函数作为参数传递以及动态调用函数等场景中非常有用。
package main
import "fmt"
// 定义一个类型为函数指针的回调函数
type Callback func(int) int
// 应用回调函数的函数
func applyCallback(x int, callback Callback) int {
return callback(x)
}
// 回调函数示例
func double(n int) int {
return n * 2
}
func main() {
result := applyCallback(5, double)
fmt.Println(result, &result) // 输出: 10 0xc00009e018
fmt.Println(double) // 输出: 0x5558980
}
Go指针示例
package main
import "fmt"
type Person struct {
Name string
Age int
}
func add(a, b int) int {
return a + b
}
func main() {
var pInt *int
pInt = new(int)
fmt.Println("整型指针:", &pInt, *pInt, pInt)
var pString *string
pString = new(string)
fmt.Println("字符串指针:", &pString, *pString, pString)
var pArr *[2]int
pArr = new([2]int)
fmt.Println("数组指针:", &pArr, *pArr, pArr)
var pSlice *[]int
pSlice = new([]int)
fmt.Println("切片指针:", &pSlice, *pSlice, pSlice)
slice := []int{1, 2, 3}
var pointSlice *[]int = &slice
fmt.Println("切片指针:", &pointSlice, *pointSlice, pointSlice)
var person Person = Person{Name: "Alice", Age: 20}
var pPerson *Person = &person
fmt.Println("结构体指针:", &pPerson, *pPerson, pPerson)
fmt.Println("函数指针:", add)
}
- 输出结果
整型指针: 0xc0000a6010 0 0xc00009e018
字符串指针: 0xc0000a6020 0xc000090020
数组指针: 0xc0000a6028 [0 0] &[0 0]
切片指针: 0xc0000a6030 [] &[]
切片指针: 0xc0000a6038 [1 2 3] &[1 2 3]
结构体指针: 0xc0000a6040 {Alice 20} &{Alice 20}
函数指针: 0xeecb980
Go应用场景
函数参数传递: 当函数参数是一个较大的结构体或数组时,传递指针而不是副本可以显著提高性能并节省内存。
package main
import (
"fmt"
)
// 定义一个结构体 LargeStruct,其中包含两个大数组字段
type LargeStruct struct {
Field1 [1024]int // 一个包含 1024 个整数的数组
Field2 [1024]int // 另一个包含 1024 个整数的数组
}
// 定义一个函数 modifyLargeStruct,用于修改 LargeStruct 结构体的内容
// 该函数接受一个指向 LargeStruct 结构体的指针
func modifyLargeStruct(ls *LargeStruct) {
// 修改 LargeStruct 结构体的第一个数组 Field1 的第一个元素
ls.Field1[0] = 999
}
func main() {
// 创建一个 LargeStruct 类型的变量 ls
ls := LargeStruct{}
// 调用 modifyLargeStruct 函数,并传递 ls 的指针
modifyLargeStruct(&ls)
// 打印 LargeStruct 结构体的 Field1 数组的第一个元素,预期输出 999
fmt.Println(ls.Field1[0]) // 输出: 999
}
共享内存: 在多线程或并发编程中,指针用于共享数据和同步状态。
package main
import (
"fmt"
"sync"
)
// 定义一个函数 increment,用于增加计数器的值
func increment(counter *int, wg *sync.WaitGroup) {
// 增加计数器的值
*counter++
// 调用 Done 方法,表示该 goroutine 已完成
wg.Done()
}
func main() {
var counter int // 定义一个计数器
var wg sync.WaitGroup // 定义一个 WaitGroup,用于等待一组 goroutines 完成执行
// 启动 1000 个 goroutine,每个 goroutine 都调用 increment 函数
for i := 0; i < 1000; i++ {
wg.Add(1) // 增加一个需要等待的 goroutine 计数
go increment(&counter, &wg)
}
// 等待所有 goroutine 完成
wg.Wait()
// 打印最终的计数器值
fmt.Println("Counter:", counter) // 输出: Counter: 989
}
- 每个 goroutine 都在同时增加 counter 变量的值,没有使用任何同步机制来保护对 counter 的访问,这会导致数据竞争。为了确保计数器的值是正确的,我们需要使用某种同步机制来保护对 counter 的访问。在 Go 语言中,可以使用 sync.Mutex 来确保一次只有一个 goroutine 能够访问和修改 counter。
package main
import (
"fmt"
"sync"
)
// 定义一个函数 increment,用于增加计数器的值
func increment(counter *int, mutex *sync.Mutex, wg *sync.WaitGroup) {
// 加锁
mutex.Lock()
// 增加计数器的值
*counter++
// 解锁
mutex.Unlock()
// 调用 Done 方法,表示该 goroutine 已完成
wg.Done()
}
func main() {
var counter int // 定义一个计数器
var wg sync.WaitGroup // 定义一个 WaitGroup,用于等待一组 goroutines 完成执行
var mutex sync.Mutex // 定义一个 Mutex,用于保护 counter 的访问
// 启动 1000 个 goroutine,每个 goroutine 都调用 increment 函数
for i := 0; i < 1000; i++ {
wg.Add(1) // 增加一个需要等待的 goroutine 计数
go increment(&counter, &mutex, &wg)
}
// 等待所有 goroutine 完成
wg.Wait()
// 打印最终的计数器值
fmt.Println("Counter:", counter) // 输出: Counter: 1000
}
实现链表等复杂数据结构: 链表、树等数据结构的实现通常需要使用指针来链接节点。
package main
import (
"fmt"
)
// 定义一个 Node 结构体,表示链表中的一个节点
type Node struct {
Value int // 节点的值
Next *Node // 指向下一个节点的指针
}
func main() {
// 创建链表的第一个节点(头节点),值为 1
head := &Node{Value: 1}
// 创建链表的第二个节点,值为 2
second := &Node{Value: 2}
// 创建链表的第三个节点,值为 3
third := &Node{Value: 3}
// 将头节点的 Next 指针指向第二个节点,形成链表的一部分
head.Next = second
// 将第二个节点的 Next 指针指向第三个节点,完成链表的构建
second.Next = third
// 遍历链表并打印每个节点的值
// 从头节点开始,逐步移动到下一个节点,直到节点为空
for n := head; n != nil; n = n.Next {
// 打印当前节点的值
fmt.Println(n.Value)
}
// 输出:
// 1
// 2
// 3
}
修改变量值: 通过指针,可以在函数中修改传入变量的值,而不仅仅是函数内部的局部副本。
package main
import (
"fmt"
)
func setToZero(x *int) {
*x = 0
}
func main() {
a := 5
setToZero(&a)
fmt.Println(a) // 输出: 0
}
减少内存分配: 在高性能场景中,频繁的内存分配和释放会影响性能,指针可以帮助减少这种开销。
package main
import (
"fmt"
)
// 定义一个结构体 Item,其中包含一个整数值的字段
type Item struct {
Value int
}
// 定义一个函数 processItem,用于处理 Item 对象
// 该函数接受一个指向 Item 结构体的指针作为参数,并将其 Value 字段设置为 42
func processItem(item *Item) {
item.Value = 42
}
func main() {
// 创建一个长度为 5 的 Item 结构体切片
items := make([]Item, 5)
// 使用 for 循环遍历切片中的每一个元素
for i := range items {
// 调用 processItem 函数处理每一个 Item 对象
// 使用 &items[i] 传递每个元素的指针,以便在函数内部可以修改原始对象
processItem(&items[i])
}
// 打印切片 items 的内容,预期输出为 [{42} {42} {42} {42} {42}]
fmt.Println(items)
}
实现回调函数: 指针用于传递函数指针,实现回调机制。
package main
import (
"fmt"
)
func apply(f func(int) int, x *int) {
*x = f(*x)
}
func double(n int) int {
return n * 2
}
func main() {
x := 5
apply(double, &x)
fmt.Println(x) // 输出: 10
}
内存映射文件(mmap): 在操作系统支持内存映射文件的情况下,指针可以用于直接访问文件内容,提高IO操作效率。
- example.txt
Golang
package main
import (
"os"
"fmt"
"syscall"
)
func main() {
// 打开文件 example.txt,以读写模式打开文件,文件权限设置为 0644
f, err := os.OpenFile("example.txt", os.O_RDWR, 0644)
if err != nil {
// 如果打开文件时发生错误,则会触发 panic 并打印错误信息
panic(err)
}
// 确保在 main 函数返回之前关闭文件
defer f.Close()
// 使用 syscall.Mmap 创建一个文件的内存映射
// 参数说明:
// int(f.Fd()): 文件描述符
// 0: 映射的起始位置(从文件头开始)
// 4096: 映射的大小(4KB)
// syscall.PROT_READ|syscall.PROT_WRITE: 内存映射区域的保护方式(可读可写)
// syscall.MAP_SHARED: 共享内存映射(对内存的修改会反映到文件中)
data, err := syscall.Mmap(int(f.Fd()), 0, 4096, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
// 如果创建内存映射时发生错误,则会触发 panic 并打印错误信息
panic(err)
}
// 确保在 main 函数返回之前解除内存映射
defer syscall.Munmap(data)
// 修改内存映射区域的数据
data[0] = 'H' // 将内存映射的第一个字节改为 'H'
data[1] = 'i' // 将内存映射的第二个字节改为 'i'
// 打印内存映射区域的内容,将其转换为字符串并打印
// 假设文件的初始内容是 "example", 修改后应输出 "Hiample"
fmt.Println(string(data)) // 输出: Hilang
}
高效的缓存和池管理: 指针用于实现对象池,以复用对象,减少GC压力。
package main
import (
"fmt"
"sync"
)
// 创建一个 sync.Pool 实例,并定义其 New 字段
// sync.Pool 是用于临时对象的池子,可以减少对象创建和垃圾回收的开销
var pool = sync.Pool{
// New 字段是一个函数,当池子为空时会调用这个函数来创建一个新的对象
New: func() interface{} {
// 返回一个指向新创建的 Object 的指针
return &Object{}
},
}
// 定义一个简单的结构体 Object
type Object struct {
Value int // 包含一个整数值的字段
}
func main() {
// 从池子中获取一个对象,如果池子为空则会调用 New 函数创建一个新对象
obj := pool.Get().(*Object)
// 设置对象的 Value 字段
obj.Value = 42
// 将对象放回池子中,这样可以在下次使用时复用这个对象
pool.Put(obj)
// 再次从池子中获取一个对象
anotherObj := pool.Get().(*Object)
// 输出对象的 Value 字段的值,这里会输出 42,因为这是之前设置的值
fmt.Println(anotherObj.Value) // 输出: 42
}