Go语言中的指针 – 你必须知道的10+件事情

290 阅读5分钟

作者:Sutirtha Chakraborty 翻译于2023/8/12

指针是编写优秀代码的最重要基础之一。在这篇文章中,我们将探讨指针,它们是什么,以及在Go中如何使用它们来编写高质量代码。

什么是指针?

指针是存储它所指向地址的变量。特定类型的指针只能指向该类型。

Golang指针语法

指针的语法非常简单。以下是Go中声明指针的语法。

var ptr *type
var ptrint *int     // 指向int的指针

指针的零值是nil。

在Go中初始化指针

某种类型的指针可以通过变量地址(&)运算符对该类型的指针进行初始化。以下是实现方法。

package main

import (
    "fmt"
)

func main() {
    var q int = 42
    var p *int     // 声明指针
    p = &q         // 初始化指针
    fmt.Println(p)  // 0x40e020
}
  1. Go中的指针解引用

解引用指针意味着获取指针所持有的地址内的值。如果我们有一个内存地址,我们可以解引用指针以获取其内部的值。以下是使用星号(*)运算符显示解引用操作的相同示例。

package main

import (
    "fmt"
)

func main() {
    var q int = 42
    var p *int
    p = &q
    fmt.Println(p)  // 0x40e020
    fmt.Println(*p) // 42
}

Go语言中的指向指针

指针变量甚至可以存储指针的地址,因为指针也是像其他变量一样的变量。因此,我们可以指向指针并创建级联间接性。这些级联间接性有时可能会引起不必要的混淆,因此在使用时要小心。

package main

import (
    "fmt"
)

func main() {   
    i := 64
    j := &i  // j是指向int的指针
    k := &j  // k是指向指向int的指针(指向另一个指针)

    fmt.Println(i)  // 64

    fmt.Println(j)  // 0x40e020 

    fmt.Println(*j) // 64(地址内的值)

    fmt.Println(k)  // 0x40c138

    fmt.Println(*k) // 0x40e020(j的地址)
}

指向接口的指针

指针甚至可以指向任何内容,甚至是接口。当指向空接口时,返回值为nil。

package main

import (
    "fmt"
)

func main() {   
    var a interface{}
    b := &a
    fmt.Println(b)    // 0x40c138
    fmt.Println(*b)   // <nil>
}

以下是带有指针的接口的示例。

package main

import (
    "fmt"
)

// 声明接口
type Bird interface {
    fly()
}

type B struct {
    name string
}

// 实现接口
func (b B) fly() {
    fmt.Println("飞翔中...")
}

func main() {   
    var a Bird = B{"孔雀"}
    b := &a
    fmt.Println(b)    // 0x40c138
    fmt.Println(*b)   // {孔雀}
}

在这段代码中,"a"是类型为Bird的结构体。这就是多态性在实际中的应用。Go允许使用接口进行多态性。因此,可以看出在Go中,指向结构体或接口的指针是一种重要的工具。

指针作为函数参数

指针可以像值一样用于函数参数。与直接使用值本身相比,指针具有一些优势。这是将大型对象传递给函数的一种非常高效的方式。使用指针可以优化程序性能。

package main

import (
    "fmt"
)

// 声明指针作为参数
func f(a *int) {
    fmt.Println(*a)
}

func main() {   
    var a int = 42

    // 传递地址
    f(&a) // 42
}

直接调用大型对象可能会拉长执行时间,以下是将指向结构体的指针传递的示例。

package main

import (
    "fmt"
)

type Human struct {
    name string
    age int
    place string
}

func f(h *Human) {
    fmt.Println("用户", (*h).name, "年龄为", (*h).age, "岁,来自", (*h).place)
}

func main() {   
    john := Human{"约翰", 36, "拉斯维加斯"}

    f(&john) // 用户约翰年龄为36岁,来自拉斯维加斯
}

在解析结构体指针时要小心。如果写成*structname.field1,会报错。正确的写法是(*structname).field1。因为指针的对象是struct,而不是struct内部的值。

在函数内部使用指针可以修改外部值(值不是常量)。当我们想要更改一个值时,我们应该使用指向该值的指针作为函数参数,然后进行必要的修改。

Go中的“new”函数

Go中的new函数返回一个指向类型的指针。

package main

import (
    "fmt"
)

func main() {   
    ptri := new(int)
    *ptri = 67

    fmt.Println(ptri)  // 0x40e020
    fmt.Println(*ptri) // 67
}

函数返回指针

任何类型的指针都可以作为值从函数中返回。与直接返回值不同,我们只需返回该值的地址。

package main

import (
    "fmt"
)

func p() *int {  // 指定返回类型为指针
    v := 101

    // 返回地址
    return &v
}

func main() {   
    n := p()
    fmt.Println(n)  // 0x40e020
    fmt.Println(*n) // 101
}

函数指针

在Go中,函数指针隐式工作。这意味着我们不需要将其声明为指针。

package main

import (
    "fmt"
)

func main() {       
    f := func() {
        fmt.Println("一个函数")   
    }
    pf := f
    pf() // 一个函数
}

在使用Go中的指针时要注意的事项

Go不允许指针算术。因此,我们不能像在C/C++中那样进行一元递增或递减。

总结

有时候会用到指向数组的指针,但这里建议使用切片。切片比指向数组的指针更加灵活。代码更加简洁,便于摸鱼

总的来说,指针在Go语言中扮演着非常重要的角色,它们可以用于提高内存效率、实现数据共享、传递函数以及创建动态数据结构等。然而,在使用指针时,需要谨慎处理,确保正确地管理内存和指针引用,以避免潜在的问题。