Golang中指针可以拥有数据吗?

Golang中指针可以拥有数据吗? 大家好!我是Go语言的新手,有一个关于指针的问题。

我正在学习这个教程:编写Web应用程序 - Go编程语言

这个函数让我有点困惑 😦

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    body, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return &Page{Title: title, Body: body}, nil
}

虽然和我的问题关系不大,但为了完整起见,这里是Page结构体:

type Page struct {
    Title string
    Body  []byte
}

由于我有C++和Rust的背景,这段代码看起来有一个明显的缺陷。它返回了一个指向在函数作用域内创建的数据的指针。

在C++或Rust中,这会导致悬垂指针,或者根本编译不通过。 这个指针是否“拥有”它所指向的数据?当这个指针在之后超出作用域时,数据会被清理吗?

这引出了更多问题。如果我有一个结构体变量,并且还有一个指向该结构体的指针呢?从垃圾回收的角度来看,哪个“拥有”该数据? 这个Playground代码让我觉得它们都“拥有”它(有点像引用计数)

Go Playground - Go编程语言

非常感谢任何见解和指点 😊

PS:需要说明的是,相比于Go语言内部的运作机制(当然也欢迎这方面的信息 👍),我更感兴趣的是发现Go语言中可能存在的“陷阱”或新手容易犯的错误。


更多关于Golang中指针可以拥有数据吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

我认为他们在这里使用指针是因为 Page 结构体可能会很大,也许是因为 Body 字段……

更多关于Golang中指针可以拥有数据吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


即使它不是悬空指针,我也不明白为什么在可以直接返回数据本身的情况下,还要返回指向某些数据的指针。

这是否是为了防止复制?用户难道不能直接为他们接收到的数据创建一个指针吗(假设他们想要或需要这样做)

在这种情况下,你的指针是在堆上分配的(用于 newmake 创建过程),当内存未被使用(未被引用)时,GC(垃圾回收器)会回收堆中的内存。

关于这一点,有一篇有趣的文章:GoLang 内存管理 - Calsoft 博客

在我编辑器中尝试这段代码时,似乎在这里返回一个指针可以让页面有一个合理的默认值:nil。如果 page == nil,那就意味着 err != nil

这是使用指针而非其指向的类型的有效理由吗?

在 Go 中,使用一个类型和指向它的指针真的有巨大区别吗?我原本以为某些函数会需要 Page 而不是 *Page,但我们不是可以直接对变量进行引用或解引用吗?

在Go语言中,指针不“拥有”数据,而是由垃圾回收器(GC)管理内存的生命周期。Go的指针更像是一种引用,当没有任何引用(包括指针、变量、结构体字段等)指向某个内存块时,GC会自动回收该内存。

关键点分析

  1. 函数返回局部变量的指针是安全的

    func createPage() *Page {
        return &Page{Title: "test", Body: []byte("content")}
    }
    

    Go编译器会执行逃逸分析,将这种局部变量分配到堆上,因此不会产生悬垂指针。

  2. 所有权模型

    • Go没有明确的所有权概念,GC跟踪所有可达的引用
    • 多个指针可以指向同一数据,GC通过可达性判断是否回收
    p1 := &Page{Title: "page1"}
    p2 := p1  // p1和p2指向同一数据
    p1 = nil  // 数据仍被p2引用,不会被回收
    
  3. 结构体变量 vs 指针

    var page1 Page          // 结构体变量
    page2 := &Page{}        // 指针
    page3 := new(Page)      // 指针
    
    // 从GC角度看:
    // - page1直接持有数据
    // - page2/page3通过指针引用数据
    // 只要任何引用存在,数据就不会被回收
    

示例:引用关系

type Container struct {
    page *Page
}

func main() {
    c := &Container{
        page: &Page{Title: "demo"},
    }
    
    p := c.page  // p和c.page指向同一数据
    c = nil      // Container可能被回收,但Page数据仍被p引用
    
    // 此时只有p引用Page数据
    // 当p超出作用域或设为nil后,Page数据才可能被GC回收
}

与C++/Rust的区别

  • C++:需要手动管理内存所有权(智能指针、new/delete)
  • Rust:编译时所有权系统,确保内存安全
  • Go:运行时GC自动管理,开发者只需关注引用关系

实际陷阱

  1. 循环引用:GC能处理大部分循环引用,但涉及系统资源(文件、网络连接)时需要手动关闭

    type Node struct {
        next *Node
    }
    
    n1 := &Node{}
    n2 := &Node{next: n1}
    n1.next = n2  // 循环引用,GC仍能回收(标记清除算法)
    
  2. 缓存中的引用:长期存活的对象可能意外保持数据活跃

    var cache = make(map[string]*Page)
    
    func getPage(title string) *Page {
        if p, ok := cache[title]; ok {
            return p  // 返回的指针使数据保持活跃
        }
        // ...
    }
    

Go的GC机制让开发者从显式内存管理中解放,但需要理解引用如何影响对象生命周期。函数返回局部变量指针是Go的常见模式,编译器会确保内存安全。

回到顶部