Golang指针

1.Go指针的基础

本文假设读者已经有部分go基础。

1.1什么是go的指针?

Go语言中的指针不能进行偏移和运算,是安全指针。

搞明白Go语言中的指针需要先知道3个概念:指针地址指针类型指针取值

1.1.1 go中的指针

Go语言中的函数传参都是值拷贝,可以通过指向该变量地址的指针变量来修改数据。

Go的指针基础操作只需要:&取地址*根据地址进行取值

1.1.2 指针地址和指针类型与取值

每个变量在运行时会有一个内存地址,根据不同架构指针有32位和64位地址的区别。

Go语言中的值类型int, float, bool, string, array, struct都有对应的指针类型。

如:*int, *int32, *string

num := 1027
var p = &num
// var p *int= &num
fmt.Printf("%p, %p, %p\n", p, &p, *(&p))
// 0xc00000a0c8, 0xc000072028, 0xc00000a0c8
fmt.Printf("%T, %T, %T\n", p, &p, *(&p))
// *int, **int, *int

**int,存储int类型的指针的指针变量。

1.2 Go指针常见细节

1.2.1 普通数据类型的副本机制

func changeNum(num int) {
    fmt.Printf("change: %d %p\n", num, &num)// change: 1027 0xc0001920b0
    num = 1025
    fmt.Printf("change: %d %p\n", num, &num)// change: 1025 0xc0001920b0
}

func main() {
    num := 1027
    fmt.Printf("main: %d %p\n", num, &num)  // main: 1027 0xc000192068
    changeNum(num)
}

拷贝新数据,分配新内存。

1.2.2 指针遵循副本机制实现数据修改

func changeNum(p *int) {
    fmt.Printf("change: %d %p %p\n", *p, p, &p)
    // change: 1027 0xc00000a0c8 0xc000072028
}

&p可以看出,指针被拷贝了一份赋值给了p,但是仍然指向同一个地址

1.2.3 空指针及判断

func main() {
    var p *int
    fmt.Println(p)  // <nil>
    if p != nil {
        fmt.Println("not nil")
    } else {
        fmt.Println("nil") 
    }
}

1.2.4 new的用途

用于内存分配,创建对应大小的空间,然后将该空间的地址赋值给p

var p *int = new(int)
p := new(int)

1.2.5 make的用途

make用于内存分配,只用于slice, map, chan的创建

slice, map, channel的使用,需要make进行初始化

1.2.6 数组指针

func change(p *[2]int) {
    (*p)[0] = 1027
    for index, value := range *p {
        fmt.Printf("index: %d, value: %d\n", index, value)
    }
}

func main() {
    arr := [2]int{1125, 1125}
    change(&arr)
    fmt.Println(arr)
}

数组量过大,给函数传递数组的时候栈帧接受不了如此大的数据量,所以需要数组指针进行传输。

1.2.7 指针数组的作用

func main() {
    var rank [5]int = [5]int{30, 50, 32, 12, 78}
    var prank [5]*int
    for i := 0; i < 5; i++ {
        prank[i] = &rank[i]
    }
    for i := 0; i < len(prank); i++ {
        for j := 0; j < len(prank)-i-1; j++ {
            if *prank[j] < *prank[j+1] {
                prank[j], prank[j+1] = prank[j+1], prank[j]
            }
        }
    }
    fmt.Println(rank)
    for i := 0; i < len(prank); i++ {
        fmt.Println(*prank[i], prank[i])
    }
}

排序的元素包含复杂结构或对象,通过指针排序可以避免不必要的内存操作

1.2.8 二级指针修改指针参数

在函数中,如果你想修改传入的指针所指向的值,你需要使用二级指针作为参数。这是因为普通的一级指针只能修改其指向的数据,而不能修改其本身的值

func change(p **int) {
    *p = &num2
}

func main() {
    p := &num1
    change(&p)
}

1.2.9 指向结构体的指针

go结构体是值类型,非引用传递

go语言允许我们可以像访问普通结构体一样访问结构体指针变量,不必使用->

原因是Go为方便访问结构体指针的成员变量,使用语法糖(Syntactic sugar)技术将instance.attribute形式转换为(*instance).attribute

ins := &T{} <==> new(T)

当然也可以使用函数封装进行初始化。

type CTFer struct {
    direction string
    rank int
}

func newCTFer(direction string, rank int) *CTFer {
    return &CTFer{direction: direction, rank: rank}
}

func main() {
    pwn1 := newCTFer("pwn1", 10)
    fmt.Println(pwn1)
}

1.2.10 变量逃逸

在Go语言中,“变量逃逸”(Escape Analysis)是指编译器分析变量生命周期的过程,以决定一个局部变量是否需要被分配在堆上,而不是栈上。逃逸分析是Go的编译器优化的一部分,它帮助管理内存分配,提高程序的运行效率。

逃逸分析的基本原则

  1. 局部变量:如果一个变量只在函数内部使用,并且在函数返回后不再被访问,那么这个变量可以安全地分配在栈上,因为它不会“逃逸”出函数的作用域。
  2. 函数返回的变量:如果一个局部变量被函数返回,或者通过引用传递给其他函数,那么这个变量可能在函数外部被访问,也就是说它逃逸出了函数的作用域。这种情况下,变量通常会被分配在堆上,以确保其生命周期超出函数调用。
  3. 全局变量:全局变量总是逃逸的,因为它们在整个程序的生命周期内都是可访问的。
  4. 循环中分配的变量:在循环中分配的变量,如果它们的生命周期超出了单次循环迭代,通常也会逃逸到堆上。

逃逸分析的重要性

  • 减少垃圾回收的压力:通过避免不必要的堆分配,可以减少垃圾回收的频率和时间,从而提高程序的运行效率。
  • 提高性能:栈分配通常比堆分配更快,因为栈的内存分配和释放是自动的,不需要复杂的垃圾回收机制。

go run -gcflags "-m -l" .\main.go

func example() *int {
    var x int = 10
    return &x // x逃逸了,因为它的地址被返回给了函数的调用者
}

func main() {
    p := example()
    fmt.Println(*p)
}

1.2.11 nil 空值/零值

Go语言中,指针、切片、映射、通道、函数和接口的零值是nil。

Go的nil与其他语言中的null由很多不同点

nil的类型为(T)(nil)

nil不可比较

fmt.Println(nil == nil)
//invalid operation: nil == nil (operator == not defined on untyped nil)

无法比较的类型的值是非法的

var s1 []int
var s2 []int
fmt.Println(s1 == s2)
// invalid operation: s1 == s2 (slice can only be compared to nil)

每个nil占用的内存大小不同

以切片nil值为例子

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

slice结构体定义如上,每个结构体字段占8字节,共占24字节

1.2.12 new与make的差异

make参考官方注释

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//
//    Slice: The size specifies the length. The capacity of the slice is
//    equal to its length. A second integer argument may be provided to
//    specify a different capacity; it must be no smaller than the
//    length. For example, make([]int, 0, 10) allocates an underlying array
//    of size 10 and returns a slice of length 0 and capacity 10 that is
//    backed by this underlying array.
//    Map: An empty map is allocated with enough space to hold the
//    specified number of elements. The size may be omitted, in which case
//    a small starting size is allocated.
//    Channel: The channel's buffer is initialized with the specified
//    buffer capacity. If zero, or the size is omitted, the channel is
//    unbuffered.
func make(t Type, size ...IntegerType) Type

相同点是第一个参数都传入的是类型,而不是值。

区别在于make的返回类型与它的参数类型相同,而不是指向该类型的指针。

make仅限用于slice, map, channel三个类型

p := new([]int)
v := make([]int, 10, 20)
fmt.Printf("%T %v\n", p, p)  // *[]int 0xc000118020
fmt.Printf("%T %v\n", v, p)  // []int &[]

new与make总结

  • new(T)返回T的指针*T并指向T的零值
  • make(T)返回初始化的T, 只能用于slice, map, channel, 要获取一个显示的指针,使用new进行分配,或者显式地使用一个变量的地址。
  • new函数分配内存,make函数初始化。

p := new([]int)  // &[]
// *p = append(*p, 1)  取消注释此处可成功
(*p)[0] = 19
// index out of range [0] with length 0

1.3 指针的注意事项

指针的初始值\零值\默认值为nil

1.3.1 不能操作没有合法指向的内存

var p *int
*p = 1027
// panic: runtime error: invalid memory address or nil pointer dereference

var p *int
p = new(int)
*p = 3
fmt.Println(*p)  // 3

1.3.2 值类型与引用类型

值类型直接存储值,通常分配在栈上

引用类型存储一个地址,地址对应的空间用于存储值,通常分配在堆上。没有任何变量引用时,该地址对应的数据空间就变成了一个垃圾,由GC来回收

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值