Go语言快速入门Go语言快速入门
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
  • 基础篇

    • 🚀 基础篇
    • 第1章 - 环境安装
    • 第2章 - Hello World
    • 第3章 - 变量与常量
    • 第4章 - 数据类型
    • 控制流程
    • 函数
    • 数组与切片
    • Map
    • 结构体
    • 指针

指针

指针是Go中理解内存和性能的关键。别怕,Go的指针比C简单多了!我当年学C语言的时候,指针这一章学得特别痛苦。但Go的指针设计得很优雅,没有指针运算,也不能随意访问内存,安全多了!

什么是指针?

指针是存储另一个变量内存地址的变量。

// 变量直接存储值
var a int = 42

// 指针存储变量的地址
var p *int = &a  // p是指向int的指针,存储a的地址

图示:

变量a:      值: 42        地址: 0xc0000140a0
指针p:      值: 0xc0000140a0  (存的是a的地址)

基本语法

var x int = 10

// & 取地址
p := &x  // p是*int类型,存储x的地址

// * 解引用(获取指针指向的值)
fmt.Println(*p)  // 10(获取p指向的值)

// 通过指针修改值
*p = 20
fmt.Println(x)  // 20(x也变了!)

类型说明

var x int = 10
var p *int = &x  // p的类型是 *int(指向int的指针)

fmt.Printf("x的值: %d\n", x)      // 10
fmt.Printf("x的地址: %p\n", &x)   // 0xc0000140a0
fmt.Printf("p的值: %p\n", p)      // 0xc0000140a0(和x的地址一样)
fmt.Printf("p指向的值: %d\n", *p) // 10

为什么需要指针?

1. 修改函数参数

Go是值传递,函数参数是副本。想修改原变量,必须用指针!

// ❌ 不能修改原变量
func double(n int) {
    n = n * 2  // 只是修改副本
}

// ✅ 使用指针修改原变量
func doublePtr(n *int) {
    *n = *n * 2  // 修改指针指向的值
}

func main() {
    x := 10
    double(x)
    fmt.Println(x)  // 10(没变)
    
    doublePtr(&x)
    fmt.Println(x)  // 20(变了!)
}

2. 避免大数据复制

type BigData struct {
    Data [1000000]int  // 很大的数组
}

// ❌ 值传递会复制整个结构体(约4MB)
func processBad(d BigData) {
    // ...
}

// ✅ 传指针只复制8字节(地址大小)
func processGood(d *BigData) {
    // ...
}

3. 表示"无值"

func find(id int) *User {
    // 找到返回指针
    if found {
        return &user
    }
    // 没找到返回nil
    return nil
}

// 使用
user := find(123)
if user == nil {
    fmt.Println("用户不存在")
} else {
    fmt.Println(user.Name)
}

new 和 make

new

new(T) 分配内存,返回 *T 类型指针,内存初始化为零值。

// new返回指针
p := new(int)    // *int类型,值为0
fmt.Println(*p)  // 0

q := new(Person) // *Person类型,所有字段为零值

make

make 只用于切片、map、channel,返回初始化好的值(不是指针)。

s := make([]int, 5)       // []int类型,长度5
m := make(map[string]int) // map[string]int类型
c := make(chan int)       // chan int类型

new vs make 对比

特性newmake
返回类型指针 *T值 T
适用类型任何类型slice、map、chan
初始化零值完全初始化
// new 和字面量创建指针等价
p1 := new(Person)
p2 := &Person{}  // 更常用

指针的零值

指针的零值是 nil。使用nil指针会panic!

var p *int
fmt.Println(p)  // <nil>

// *p = 10  // ❌ panic: runtime error: invalid memory address

// 使用前要检查
if p != nil {
    fmt.Println(*p)
}

函数中的指针

传值 vs 传指针

// 传值:创建副本
func modifyValue(s Student) {
    s.Age = 100  // 只修改副本
}

// 传指针:操作原数据
func modifyPointer(s *Student) {
    s.Age = 100  // 修改原数据
}

func main() {
    s := Student{Name: "张三", Age: 20}
    
    modifyValue(s)
    fmt.Println(s.Age)  // 20(没变)
    
    modifyPointer(&s)
    fmt.Println(s.Age)  // 100(变了)
}

返回局部变量的指针

在Go中完全安全!Go会自动将变量分配到堆上(逃逸分析)。

// ✅ 完全安全
func newPerson(name string) *Person {
    p := Person{Name: name}
    return &p  // 返回局部变量的指针
}

// Go编译器会自动把p分配到堆上,不用担心

和C的区别

在C语言中,返回局部变量的指针是危险的(野指针)。 Go有垃圾回收,自动处理内存,你可以放心使用!

指针和结构体

type Person struct {
    Name string
    Age  int
}

func main() {
    // 创建结构体
    p := Person{Name: "张三", Age: 25}
    
    // 获取指针
    ptr := &p
    
    // 通过指针访问字段(Go自动解引用)
    fmt.Println(ptr.Name)  // 张三
    // 等价于
    fmt.Println((*ptr).Name)  // 张三
    
    // 修改
    ptr.Age = 26
    fmt.Println(p.Age)  // 26
}

指针数组 vs 数组指针

// 指针数组:存储指针的数组
var ptrArr [3]*int
a, b, c := 1, 2, 3
ptrArr = [3]*int{&a, &b, &c}

// 数组指针:指向数组的指针
arr := [3]int{1, 2, 3}
var arrPtr *[3]int = &arr

二级指针

指向指针的指针(用得不多):

a := 10
p := &a   // 一级指针
pp := &p  // 二级指针

fmt.Println(**pp)  // 10

// 修改
**pp = 20
fmt.Println(a)  // 20

常见使用场景

1. 方法接收者

type Counter struct {
    count int
}

// 值接收者:不能修改
func (c Counter) GetCount() int {
    return c.count
}

// 指针接收者:可以修改
func (c *Counter) Increment() {
    c.count++
}

func main() {
    c := Counter{}
    c.Increment()
    c.Increment()
    fmt.Println(c.GetCount())  // 2
}

2. 链表结构

type Node struct {
    Value int
    Next  *Node  // 指向下一个节点
}

func main() {
    // 创建链表: 1 -> 2 -> 3
    n1 := &Node{Value: 1}
    n2 := &Node{Value: 2}
    n3 := &Node{Value: 3}
    n1.Next = n2
    n2.Next = n3
    
    // 遍历链表
    for n := n1; n != nil; n = n.Next {
        fmt.Println(n.Value)
    }
}

3. 树结构

type TreeNode struct {
    Value int
    Left  *TreeNode
    Right *TreeNode
}

// 创建二叉树
//       1
//      / \
//     2   3
root := &TreeNode{
    Value: 1,
    Left:  &TreeNode{Value: 2},
    Right: &TreeNode{Value: 3},
}

实战案例:实现链表

package main

import "fmt"

// 链表节点
type ListNode struct {
    Value int
    Next  *ListNode
}

// 链表
type LinkedList struct {
    Head *ListNode
    size int
}

// 创建新链表
func NewLinkedList() *LinkedList {
    return &LinkedList{}
}

// 添加到末尾
func (l *LinkedList) Append(value int) {
    newNode := &ListNode{Value: value}
    
    if l.Head == nil {
        l.Head = newNode
    } else {
        current := l.Head
        for current.Next != nil {
            current = current.Next
        }
        current.Next = newNode
    }
    l.size++
}

// 在头部添加
func (l *LinkedList) Prepend(value int) {
    newNode := &ListNode{Value: value, Next: l.Head}
    l.Head = newNode
    l.size++
}

// 删除节点
func (l *LinkedList) Delete(value int) bool {
    if l.Head == nil {
        return false
    }
    
    // 删除头节点
    if l.Head.Value == value {
        l.Head = l.Head.Next
        l.size--
        return true
    }
    
    // 删除其他节点
    current := l.Head
    for current.Next != nil {
        if current.Next.Value == value {
            current.Next = current.Next.Next
            l.size--
            return true
        }
        current = current.Next
    }
    return false
}

// 查找节点
func (l *LinkedList) Find(value int) *ListNode {
    current := l.Head
    for current != nil {
        if current.Value == value {
            return current
        }
        current = current.Next
    }
    return nil
}

// 获取长度
func (l *LinkedList) Size() int {
    return l.size
}

// 转换为切片
func (l *LinkedList) ToSlice() []int {
    result := []int{}
    current := l.Head
    for current != nil {
        result = append(result, current.Value)
        current = current.Next
    }
    return result
}

// 打印链表
func (l *LinkedList) Print() {
    current := l.Head
    for current != nil {
        fmt.Print(current.Value)
        if current.Next != nil {
            fmt.Print(" -> ")
        }
        current = current.Next
    }
    fmt.Println()
}

// 反转链表
func (l *LinkedList) Reverse() {
    var prev *ListNode
    current := l.Head
    
    for current != nil {
        next := current.Next
        current.Next = prev
        prev = current
        current = next
    }
    l.Head = prev
}

func main() {
    list := NewLinkedList()
    
    // 添加元素
    list.Append(1)
    list.Append(2)
    list.Append(3)
    list.Prepend(0)
    
    fmt.Print("链表: ")
    list.Print()  // 0 -> 1 -> 2 -> 3
    
    fmt.Printf("长度: %d\n", list.Size())  // 4
    
    // 查找
    if node := list.Find(2); node != nil {
        fmt.Printf("找到节点: %d\n", node.Value)
    }
    
    // 删除
    list.Delete(2)
    fmt.Print("删除2后: ")
    list.Print()  // 0 -> 1 -> 3
    
    // 反转
    list.Reverse()
    fmt.Print("反转后: ")
    list.Print()  // 3 -> 1 -> 0
    
    fmt.Println("转切片:", list.ToSlice())  // [3 1 0]
}

输出:

链表: 0 -> 1 -> 2 -> 3
长度: 4
找到节点: 2
删除2后: 0 -> 1 -> 3
反转后: 3 -> 1 -> 0
转切片: [3 1 0]

实战案例:实现二叉搜索树

package main

import "fmt"

// 二叉搜索树节点
type BSTreeNode struct {
    Value int
    Left  *BSTreeNode
    Right *BSTreeNode
}

// 二叉搜索树
type BSTree struct {
    Root *BSTreeNode
}

// 创建新树
func NewBSTree() *BSTree {
    return &BSTree{}
}

// 插入节点
func (t *BSTree) Insert(value int) {
    t.Root = insertNode(t.Root, value)
}

func insertNode(node *BSTreeNode, value int) *BSTreeNode {
    if node == nil {
        return &BSTreeNode{Value: value}
    }
    
    if value < node.Value {
        node.Left = insertNode(node.Left, value)
    } else if value > node.Value {
        node.Right = insertNode(node.Right, value)
    }
    return node
}

// 查找节点
func (t *BSTree) Search(value int) bool {
    return searchNode(t.Root, value)
}

func searchNode(node *BSTreeNode, value int) bool {
    if node == nil {
        return false
    }
    if value == node.Value {
        return true
    }
    if value < node.Value {
        return searchNode(node.Left, value)
    }
    return searchNode(node.Right, value)
}

// 中序遍历(有序输出)
func (t *BSTree) InOrder() []int {
    result := []int{}
    inOrderTraverse(t.Root, &result)
    return result
}

func inOrderTraverse(node *BSTreeNode, result *[]int) {
    if node == nil {
        return
    }
    inOrderTraverse(node.Left, result)
    *result = append(*result, node.Value)
    inOrderTraverse(node.Right, result)
}

// 获取最小值
func (t *BSTree) Min() (int, bool) {
    if t.Root == nil {
        return 0, false
    }
    node := t.Root
    for node.Left != nil {
        node = node.Left
    }
    return node.Value, true
}

// 获取最大值
func (t *BSTree) Max() (int, bool) {
    if t.Root == nil {
        return 0, false
    }
    node := t.Root
    for node.Right != nil {
        node = node.Right
    }
    return node.Value, true
}

func main() {
    tree := NewBSTree()
    
    // 插入节点
    values := []int{50, 30, 70, 20, 40, 60, 80}
    for _, v := range values {
        tree.Insert(v)
    }
    
    //        50
    //       /  \
    //      30   70
    //     / \   / \
    //    20 40 60 80
    
    fmt.Println("中序遍历(有序):", tree.InOrder())  // [20 30 40 50 60 70 80]
    
    fmt.Println("查找40:", tree.Search(40))  // true
    fmt.Println("查找45:", tree.Search(45))  // false
    
    if min, ok := tree.Min(); ok {
        fmt.Println("最小值:", min)  // 20
    }
    
    if max, ok := tree.Max(); ok {
        fmt.Println("最大值:", max)  // 80
    }
}

指针使用建议

什么时候用指针?

场景是否用指针原因
需要修改参数✅ 用指针值传递无法修改原变量
大结构体✅ 用指针避免大量复制
小结构体(几个字段)❓ 视情况值传递也可以
基本类型(int、string)❌ 不用复制开销小
切片、map、channel❌ 不用本身就是引用类型

常见错误

// ❌ 错误:使用nil指针
var p *int
*p = 10  // panic!

// ✅ 正确:先初始化
p := new(int)
*p = 10

// ❌ 错误:循环中取地址
var ptrs []*int
for i := 0; i < 3; i++ {
    ptrs = append(ptrs, &i)  // 都指向同一个i!
}

// ✅ 正确:创建新变量
for i := 0; i < 3; i++ {
    v := i
    ptrs = append(ptrs, &v)
}

练习

  1. 写一个函数,交换两个整数的值(使用指针)
  2. 实现一个函数,找出切片中的最大值,返回其指针(找不到返回nil)
  3. 实现双向链表的基本操作
参考答案
package main

import "fmt"

// 1. 交换两个整数
func swap(a, b *int) {
    *a, *b = *b, *a
}

// 2. 找最大值返回指针
func findMax(nums []int) *int {
    if len(nums) == 0 {
        return nil
    }
    maxIdx := 0
    for i, n := range nums {
        if n > nums[maxIdx] {
            maxIdx = i
        }
    }
    return &nums[maxIdx]
}

// 3. 双向链表
type DNode struct {
    Value int
    Prev  *DNode
    Next  *DNode
}

type DoublyLinkedList struct {
    Head *DNode
    Tail *DNode
    size int
}

func NewDoublyLinkedList() *DoublyLinkedList {
    return &DoublyLinkedList{}
}

func (l *DoublyLinkedList) Append(value int) {
    node := &DNode{Value: value}
    if l.Tail == nil {
        l.Head = node
        l.Tail = node
    } else {
        node.Prev = l.Tail
        l.Tail.Next = node
        l.Tail = node
    }
    l.size++
}

func (l *DoublyLinkedList) Prepend(value int) {
    node := &DNode{Value: value}
    if l.Head == nil {
        l.Head = node
        l.Tail = node
    } else {
        node.Next = l.Head
        l.Head.Prev = node
        l.Head = node
    }
    l.size++
}

func (l *DoublyLinkedList) PrintForward() {
    current := l.Head
    for current != nil {
        fmt.Print(current.Value)
        if current.Next != nil {
            fmt.Print(" <-> ")
        }
        current = current.Next
    }
    fmt.Println()
}

func (l *DoublyLinkedList) PrintBackward() {
    current := l.Tail
    for current != nil {
        fmt.Print(current.Value)
        if current.Prev != nil {
            fmt.Print(" <-> ")
        }
        current = current.Prev
    }
    fmt.Println()
}

func main() {
    // 1. 测试swap
    a, b := 10, 20
    swap(&a, &b)
    fmt.Printf("交换后: a=%d, b=%d\n", a, b)  // a=20, b=10
    
    // 2. 测试findMax
    nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
    if max := findMax(nums); max != nil {
        fmt.Printf("最大值: %d\n", *max)  // 9
    }
    
    // 3. 测试双向链表
    dll := NewDoublyLinkedList()
    dll.Append(1)
    dll.Append(2)
    dll.Append(3)
    dll.Prepend(0)
    
    fmt.Print("正向: ")
    dll.PrintForward()  // 0 <-> 1 <-> 2 <-> 3
    
    fmt.Print("反向: ")
    dll.PrintBackward()  // 3 <-> 2 <-> 1 <-> 0
}

🎉 恭喜你完成了基础篇!你已经掌握了Go的核心语法。

接下来进入 进阶篇,学习更强大的特性:方法、接口、并发编程!

最近更新: 2025/12/27 13:26
Contributors: 王长安
Prev
结构体