Go 语言学习指南:从 Java 到 Golang 的思维转变

前段时间入职节子,快速过了一下 Go,顺便整理出了一篇博客,全文共计 67803 字,适合有一定语言基础,想快速建立全局观、快速入门 go的读者

了解go

go的设计哲学是“少即是多”(Less is More),它刻意移除了许多 Java 中常见的特性(如类继承、复杂的异常体系等),以换取更简单的代码结构和更高的编译/运行效率

优势:

  1. 极简的部署方式:

    1. 可直接编译成机器码
    2. 不依赖其他第三方库
    3. 直接运行即可部署
  2. 静态性语言

    1. 编译的时候能检查出隐藏的大多数问题
  3. 支持语言层面的并发

    1. 原生支持:原生的语法就支持并发

      go
      package main
      import (
      	"fmt"
        	"time"
      )
      func goFunc(i int){
        fmt.Println("goroutine", i, "...")
      }
      
      func main(){
        for i := 0;i<10000; i++ {
          go goFunc(i)// 开启一个并发协程
        }
        time.Sleep(time.Second)
      }
      

      不需要绑定 CPU,不需要配置线程池的一堆参数(核心线程数、非核心线程数……)

    2. 充分利用多核 CPU

  4. 强大的标准库

    1. runtime 系统调度机制

    2. 高效的 GC 垃圾回收

      1.8 之后引入三色标记、混合写屏障

    3. 丰富的标准库:
      文本操作、输入输出、时间日期、网络通信、统计值、数学……
      一般来说原生的标准库就能满足开发需求,除非有特殊需求 需要一些在某些性能/特点上更突出的第三方库

  5. 简单易学

    1. 25 个关键字
    2. C 语言简洁基因,内嵌 C 语法支持
    3. 面向对象
    4. 跨平台

各语言编译、运行耗时粗略对比:

应用场景:

  • 云计算基础设施领域
    代表项目:docker、kubernetes、etcd、cloudflare CDN……
  • 基础后端软件
    代表项目:tidb、influxdb、cockroachdb……
  • 微服务
    代表项目:go-kit、micro、monzo back的 typhon、bilibili……
  • 互联网基础设施
    代表项目:以太坊、hyperledger 等

缺点:

  • Go 大部分包都在 github 上,相对不太安全
  • 所有异常都用 Error 来处理:没有try catch语法
    (比较有争议)
  • 对 C 的降级处理并非无缝(不像 C++一样完美兼容)

安装go

  1. 平台:https://go.dev/dl/https://goframe.org/docs/install-go/index

  2. 检查是否安装成功

    bash
    C:\Users\Hazenix>go version
    go version go1.26.1 windows/amd64
    C:\Users\Hazenix>go --help
    

几个环境变量:

  • GOROOT:源码包的安装路径
  • GOPATH:当前用户的工作路径(写代码的地方),go install 安装的工具在 GOPATH/bin/(使用模块化管理则不需要配这个环境变量)

工具与命令

在线练习工具

开发工具

  • GoLand(JetBrains出品,功能强大)
  • Visual Studio Code + Go插件
  • Vim/Neovim + LSP支持

常用命令

命令 功能
go run main.go 直接运行Go源码
go build main.go 编译生成可执行文件 main.go 编译完生成 main 文件;直接./main就能执行
go fmt 格式化代码
go mod init 初始化模块
go test ./… 运行所有测试
go get http://github.com/user/repo 下载第三方依赖

一、基础语法

Hello world

go
package main // 当前属于的包,和文件名无必然关系

import "fmt" // format库

// 默认找到 main 函数执行,只能有一个 main 函数
func main(){
    // 调用 fmt 包中的 Println() 函数
    fmt.Println("Hello, World")
}

  • go 语言句末不需要分号
  • 强制的代码风格要求:左大括号 { 和函数名一定是在同一行,否则编译报错

💡 为什么 { 必须和函数名在同一行? Go 的词法器会在扫描源码时自动插入分号。规则是:如果换行前的最后一个 token 是标识符、数字/字符串字面量、或 break continue fallthrough return ++ -- ) } 之一,词法器会在其后自动插入分号。(这个规则同样适用于 ifforswitch

所以如果写:

go
func main()
{                         // ← 词法器在 ) 后插入分号 → func main(); { ... 编译错误!

所以 { 必须紧跟在函数签名后面。Go 代码中分号只出现在 for 循环子句等少数场景。

1. 变量与常量

go
// 声明变量的几种方式
// 1. 全写
var name string = "Alice"
var age int = 30
var isActive bool

// 2. 直接初始化 自动识别类型
var name = "Alice"

// 3. 使用`:=`(常用)
name := "Alice"
age := 30



// 多变量声明
// 1. 标注类型
var c, python ,java bool // type 放多个变量的末尾
var x, y int = 1, 2 

// 2. 直接初始化 自动识别类型
var rust, golang, javascript  = true, false, "no!"
// If an initializer is present, the type can be omitted(省略); the variable will take the type of the initializer.

// 3. 使用 := 与 , 的形式
a, b := 10, 20

// 4. 块式批量声明
var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)


// 常量(只读)
// 1. 标注类型
const PI float64 = 3.14159
// 2. 直接初始化 自动识别类型
const PI = 3.14159
// 3. 块式批量声明
const (
    STATUSOK = 200
    STATUSNOTFOUND = 404
)

// iota 常量生成器(Go 特有)
const (
    SONDAY = iota  // 0
    MONDAY         // 1
    TUESDAY        // 2
)

💡 注意事项:

  • := 只能在函数内部使用,全局变量必须用 var 关键字声明

  • 自动化类型推断:

    当在声明变量时未显式指定类型(无论是 := 语法,还是使用 var = 表达式语法),变量的类型都会根据右侧的值自动推断。

    当声明右侧的值本身带有类型时,新变量就会具有相同的类型:

    go
    var i int
    j := i // j is an int
    

    当右侧包含一个无类型的数字常量时,新变量的类型会根据该常量的精度被推断为 intfloat64complex128

    go
    i := 42           // int
    f := 3.142        // float64
    g := 0.867 + 0.5i // complex128
    
  • 声明的变量必须使用,否则编译报错

  • := 的重新赋值规则:= 声明中,只要至少有一个新变量,已存在的变量可以被重新赋值(而非重新声明),前提是在同一作用域

    go
    f, err := os.Open(name)    // 声明 f 和 err
    d, err := f.Stat()         // d 是新变量,err 只是重新赋值 ✅
    // 注意:如果 err 在外部作用域已声明,这里会创建一个新的同名局部变量(shadowing)⚠️
    

    这个特性让长链的错误处理变得简洁:不需要为每次调用声明 err1, err2, err3...,可以一直复用 err

  • 常量在编译时确定,不能是函数返回值;常量的定义不能使用 := 语法

2. 几种数据类型

go
// 基本类型
var (
    num     int
    decimal float64
    flag    bool
    text    string
)

string

go
package main

import (
    "fmt"
    "strings"
)

func main(){
    s := "hello, world"
    // 获取长度
    fmt.Println(len(s)) // len() 是内置函数,不需要导包
    
    // 转成大写(需要使用到strings库)
    fmt.Println(strings.ToUpper(s))
    
    fmt.Println(strings.Contains(s, "world"))
    
    fmt.Println(strings.Split(s, ","))
    
    // 把hello替换成你好
    fmt.Println(strings.ReplaceAll(s,"hello", "你好"))
    
}

数组:

go
// 数组
package main

import "fmt"

func main() {
    // 1. 先声明、再赋值
    // var arr [length]type
    var arr [3]int
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1])// Hello World
    fmt.Println(a)		   // [Hello World]
    fmt.Println(len(a))	   // 2

    // 2. 定义的时候直接初始化(指定长度)
    primes := [6]int{2, 3, 5, 7, 11, 13}
    fmt.Println(primes)		// [2 3 5 7 11 13]
    
    // 3. 定义的时候直接初始化(不指定长度)【注意与下文切片的语法作区分】
    arr2 := [...]int{1, 2, 3, 4} // 长度依然确定,为4
    
    // 数组的遍历:
    for i:=0; i<len(primes);   i++ {
        fmt.Println(primes[i])
    }
    for index, value := range primes {
        fmt.Println(index, value)
    }
}
  • 数组的长度是类型的一部分,不能重新调整

    “declares a variable arr as an array of three integers.An array’s length is part of its type, so arrays cannot be resized.”

  • 入参是数组 的函数,使用的是值传递

Slice(切片):

切片,就是动态数组,相当于 Java/Python 里面的 List

切片的定义:A slice literal is like an array literal without the length.

go
arr1 := [3]bool{true, true, false} // 创建的是数组
arr2 := []bool{true, true, false}  // 创建的是切片
go
// 切片

package main

import "fmt"

func main() {
    // 定义切片
  	// 1. 声明切片,后初始化
  	var slice []int // 此时 slice == nil
  	slice = make([]int, 3)
  	 
    // 2. 声明并初始化
    slice := []int{1, 2, 3}

    primes := [6]int{2, 3, 5, 7, 11, 13}

    // 截取切片
    // 通过指定上界和下界来截取切片:a[low:high]
    // 省略上界或下界时,使用的是默认值。下界的默认值是 0,上界的默认值是该切片的长度
    var s []int = primes[1:4] // 左闭右开
    fmt.Println(s) // [3 5 7]
}


Go 语言的切片不是一个简单数组,是结构体,组成如下:

  1. 指向底层数组的指针 :这个指针指向切片所引用的底层数组的起始位置

    "A slice does not store any data, it just describes a section of an underlying array.

    Changing the elements of a slice modifies the corresponding elements of its underlying array.

    Other slices that share the same underlying array will see those changes."

    go
    package main
    
    import "fmt"
    
    func printArray(myArray []int) {
      
    	// _ 表示匿名的变量
    	for _, value := range myArray {
    		fmt.Println("value =", value)
    	}
    	// 传递方式:实际上这里传递的传值方式仍然是按值传递,类型是切片,形参接受到的是一个切片副本,切片内部维护了指向底层数组的指针、切片长度、容量,【所以形参和原变量指向的都是同一个底层数组,所以更改myArray[0] 原切片myArray[0]也会改变】
    	myArray[0] = 100
    }
    
    func main() {
    	myArray := []int{1, 2, 3, 4} // 动态数组,切片 slice
    
    	printArray(myArray)
    
    	fmt.Println(" === ")
    
    	for _, value := range myArray {
    		fmt.Println("value =", value)
    	}
    }
    
    // 输出:
    // value = 1
    // value = 2
    // value = 3
    // value = 4
    // value = 100
    // value = 2
    // value = 3
    // value = 4
    
  2. 长度 :切片中当前元素的数量

  3. 容量:从切片起点到其底层数组末尾的元素数量。

切片的长度与容量:

切片(slice)同时具有长度(length)容量(capacity)。 切片的长度是它所包含的元素个数。 切片的容量是其底层数组中,从切片的第一个元素开始到底层数组末尾这段范围内的元素个数。 切片 s 的长度和容量可以分别通过表达式 len(s)cap(s) 获取。

go
package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13} 
	printSlice(s) // len=6 cap=6 [2 3 5 7 11 13]

  	// s[x:y]表示截取出下标 x ~ y-1 的元素
	// Slice the slice to give it zero length.
	s = s[:0]
	printSlice(s) // len=0 cap=6 []

	// Extend its length.
	s = s[:4]
	printSlice(s) // len=4 cap=6 [2 3 5 7]

	// Drop its first two values.
	s = s[2:]
	printSlice(s) // len=2 cap=4 [5 7]
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

截取使用的是浅拷贝;但是可以使用 copy 函数将底层数组的一起拷贝(深拷贝)

go
func main() {
	s := []int{2, 3, 5, 7, 11, 13} 
	s1 = s[0:2]
       s2 := make([]int, len(s)) // [0 0 0 0 0 0]
       copy(s2, s)// 将 s 的值深拷贝到 s2 中
       s1[0] = 100
	fmt.Println(s) // [100 3 5 7 11 13]
   	fmt.Println(s1)// [100 3]
   	fmt.Println(s2)// [2 3 5 7 11 13]
    }

向切片中添加新元素: func append(s []T, vs ...T) []T

返回的切片包含原切片的所有元素及追加值。(如果底层数组空间不足以容纳所有新增的值,就会分配一个更大的新数组返回的切片将会指向这个新分配的数组

(切片的更多使用:可以读这篇文章 Slices: usage and internals)

go
package main

import "fmt"

func main() {
	var s []int
	printSlice(s) // len=0 cap=0 []

	s = append(s, 0) // 重新赋值给原切片
	printSlice(s) // len=1 cap=1 [0]

	// 自动扩容
	s = append(s, 1)
	printSlice(s) // len=2 cap=2 [0 1]

	// 一次性增加多个元素
	s = append(s, 2, 3, 4)
	printSlice(s) // len=5 cap=6 [0 1 2 3 4]
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

使用make创建切片

go
a := make([]int, 5)  // len(a)=5

可以给make传递第三个参数,来指定容量(默认情况下 len = cap)

go
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

切片可以存储任何类型(包括存储其他切片)

go
package main

import (
	"fmt"
	"strings"
)

func main() {
	// Create a tic-tac-toe board.
	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	// The players take turns.
	board[0][0] = "X"
	board[2][2] = "O"
	board[1][2] = "X"
	board[1][0] = "O"
	board[0][2] = "X"

	for i := 0; i < len(board); i++ {
		fmt.Printf("%s\n", strings.Join(board[i], " "))
	}
}

Map(映射)

go
package main

import "fmt"

type Vertex struct {
	Lat, Long float64
}
// 1. 直接声明(var m map[KeyType]ValueType)
var m map[string]int		// 无法直接使用,需要用 make 开辟空间才能用
m = make(map[string]int, 10)
m["go"] = 1
m["java"] = 2

// 2. 使用 make 声明,然后可以直接赋值
m1 = make(map[int]string)
m1[1] = "java"
m1[2] = "go"
m2 = make(map[string]Vertex)
m2["Bell Labs"] = Vertex{
  40.68433, -74.39967,
}

// 3. 声明的同时完成初始化
m3 := map[string]string {
  "one": "php",
  "two": "go",
  "three": "Java"
}
m4 = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},// Vertex 这里只表示类型名,可以省略(不省略的写法:"Bell Labs": Vertex{40.68433, -74.39967})
	"Google":    {37.42202, -122.08408},
}

// 遍历
for key, value := range m1 {
  fmt.Println("key= ", key)
  fmt.Println("value= ", value)
}

// 删除
delete(cityMap, "China")

// 修改
m["go"] = 0

// 获取 value
elem := m["go"]

// 可以通过双返回值赋值语句 elem, ok := m[key] 来检查 Map 中是否存在某个键
elem1, ok := m["Java"] // 如果存在这个 key,ok 为 true,否则为 flase
// 如果不存在这个 key,elem1 就是当前类型的零值(int 里面就是 0)

使用 Map 实现字母计数

go
func WordCount(s string) map[string]int {
    counts := make(map[string]int)
    for _, word := range strings.Fields(s) {
        counts[word]++
    }
    return counts
}

说明:

  1. 创建 mapcounts := make(map[string]int) 初始化一个空映射,用于存储每个单词及其出现次数。
  2. 分割字符串strings.Fields(s) 将输入字符串按空白字符(空格、制表符、换行等)分割成单词切片。
  3. 遍历计数:对每个单词,在 map 中将其计数加一。如果单词首次出现,Go 会自动初始化为 0,然后 +1。
  4. 返回结果:最终返回包含所有单词计数的 map。

指针

Go 语言里也有指针。指针保存的是某个值在内存中的地址。 类型 *T 表示“指向 T 类型值的指针”,它的零值是 nil

go
var p *int

**&是取址符,&p取到的是局部变量p的地址 **

go
i := 42
p = &i			// &运算符返回其操作数(变量)的内存地址,从而生成一个指针。

*** 是间接寻址符,p代表取p变量所存地址中的值* (也称为“解引用”/“间接寻址”)

go
fmt.Println(*p) // read i through the pointer p
*p = 21         // set i through the pointer p

解引用后,可以像操作普通变量一样读写该内存位置的值。

相比于 C,go 不支持指针类型的运算,比如 p + 1p++p-q

这是为了安全性和简化内存模型,避免越界访问和复杂错误。
Go 设计哲学是“简单、安全、高效”。指针算术容易出错(如缓冲区溢出),所以被刻意省略了。

go
package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j
}

函数值 (Function values)

函数本身也是一种值。它们可以像其他值一样被传来传去。

函数值可以作为函数的参数,也可以作为函数的返回值。

go
package main

import (
	"fmt"
	"math"
)

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12)) //13

	fmt.Println(compute(hypot)) // 5
	fmt.Println(compute(math.Pow)) // 81
}

总结

类型 说明 零值
int / int8 / int16 / int32 / int64 整数 0
uint / uint8 / uint16 / uint32 / uint64 / uintptr
byte alias for uint8
rune alias for int32, represents a Unicode code point
complex64 / complex128
float32 / float64 浮点数 0.0
bool 布尔 false
string 字符串 ""
slice 切片 nil
map 映射 nil
pointer 指针 nil

intuintuintptr 类型在 32 位系统上通常是 32 位宽,在 64 位系统上通常是 64 位宽。需要整数值时,应该使用 int,除非你有明确理由使用指定位数或无符号整数类型。

Go 中数据类型的一些特性:

  • 零值0 for numeric types, false for the boolean type, and "" (the empty string) for strings …

    Variables declared without an explicit initial value are given their zero value.

  • new vs make — 两个分配原语的区别

    Go 有两个内存分配函数,作用不同:

    特性 new(T) make(T, args...)[常用]
    适用范围 任意类型 仅 slice / map / chan
    返回值 *T(指针) T(值本身,非指针)
    初始化 清零(只分配内存,填零值) 初始化(分配内存并初始化内部结构)
    使用场景 很少直接用,通常用 &T{} 代替 slice / map / chan 的标准创建方式

    make 不返回指针的原因:这三种类型本身就是引用类型,底层数据结构已经是指针了

    go
    // new:返回指针,内存清零
    p := new(int)       // p 是 *int,*p == 0
    
    // new 很少单独使用,一般用复合字面量取地址代替:
    user := &User{Name: "Alice"}  // 等价于先 new 再赋值,但更简洁
    
    // make:只用于 slice / map / chan,返回已初始化的值
    s := make([]int, 0, 10)   // len=0, cap=10 的切片
    m := make(map[string]int) // 可用的空 map
    ch := make(chan int, 5)   // 容量 5 的 channel
    
    // 常见错误:
    // var m map[string]int     // m == nil,不能直接赋值
    // m["key"] = 1             // panic!
    // 正确写法:m := make(map[string]int)
    
  • 无隐式类型转换intint64 不能直接运算,需显式转换 int64(num)

    表达式 T(v) 能将值 v 转换为类型 T

    一些数值类型转换示例:

    go
    var i int = 42
    var f float64 = float64(i)
    var u uint = uint(f)
    

    更简单的写法:

    go
    i := 42
    f := float64(i)
    u := uint(f)
    
  • 字符串不可变:修改字符串需转为 []byte[]rune

  • nil 不能直接用:切片、map、channel 默认为 nil,需初始化才能使用

2.5 自定义类型(Named Types)

Go 中有很多预先定义好的类型(intstring[]byte 等),但你也可以用 type 关键字基于已有类型创建新的命名类型

语法

go
type 新名字 底层类型
go
// 基于内置类型创建有业务含义的新类型
type UserID int          // UserID 的底层类型是 int
type Score float64       // Score 的底层类型是 float64

// 基于复合类型创建
type StringSlice []string          // 底层类型是 []string
type UserMap map[int]string        // 底层类型是 map[int]string
type Handler func(w, r any) error  // 底层类型是函数签名

为什么需要命名类型?

① 赋予类型业务含义,提高代码可读性

go
// 不用命名类型:看不出 int 代表什么
func GetUser(id int, age int) {}

// 用命名类型:一目了然
type UserID int
type Age int

func GetUser(id UserID, age Age) {}

② 类型安全 — 防止混用

同样是 int,但 UserIDAge不同的类型,不能直接互相赋值:

go
var uid UserID = 1
var a Age = 25
uid = a           // ❌ 编译错误:cannot use a (type Age) as type UserID
uid = UserID(a)   // ✅ 显式转换后可以(底层类型相同)

③ 给类型绑定方法(Go OOP 的基础)

命名类型可以定义方法,这是 Go 面向对象编程的基石:

go
type Celsius float64

// Celsius 就有了自己的方法,就像 Java 中类的实例方法
func (c Celsius) String() string {
    return fmt.Sprintf("%.1f°C", c)
}

func main() {
    t := Celsius(36.5)
    fmt.Println(t.String()) // "36.5°C"
}

注意:只能给当前包内定义的类型绑定方法,不能给其他包的类型(包括内置类型 int)直接加方法。这就是为什么需要 type UserID int —— 先创建自己的命名类型,再给它加方法。

命名类型 vs 类型别名

Go 1.9 引入了类型别名,语法是 type Name = UnderlyingType(多一个 =):

go
type MyInt int     // 命名类型:MyInt 和 int 是不同类型,需要显式转换
type MyInt = int   // 类型别名:MyInt 就是 int 的另一个名字,可以互换

绝大多数情况下用的是命名类型(不带 =

和底层类型之间的转换

命名类型可以和它的底层类型互相转换:

go
type Count int

var c Count = 10
var i int = int(c)     // Count → int(显式转换)
var c2 Count = Count(5) // int → Count(显式转换)

// 底层类型相同但命名不同的两个类型
type Apples int
type Oranges int
var a Apples = 5
var o Oranges = Oranges(a) // 允许直接转换

type Xxx struct{...}type Xxx interface{...}type Xxx int 等常见写法 都是 type 关键字创建命名类型的应用。

3. 流程控制

if - else

go


x, n := 2, 3
// go里面条件表达式不需要用括号包裹
if v := math.Pow(x, n); v < lim {
    return v
}
// go允许在条件表达式之前定义局部变量(if 语句也可以在条件判断前先执行一条简短语句。语句中声明的局部变量 作用域只到这个 if 语句结束)


fmt.Printf(v);// 这句会引发报错



if score := getScore(); score >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}
// score 的作用域仅限于 if-else 块


// 在 `if` 的简短语句中声明的变量,在任何对应的 `else` 代码块中也可以使用
if v := math.Pow(x, n); v < lim {
    fmt.Printf(v)
} else {
    fmt.Printf("%g >= %g\n", v, lim)
}

for

go
// for 循环(三种形式)
for i := 0; i < 10; i++ {// 传统循环
    fmt.Println(i)
};

// range 遍历
for i, v := range slice {
    fmt.Println(i, v)
}
for k, v := range map {
    fmt.Println(k, v)
}

// Go 里面的 for 关键字能直接起到 while 的作用(只有一个条件表达式的时候)
for i < 10 {
    
}                    


//无限循环
for {
    
}

注意

  • for 循环的三个表达式不能用括号包裹

  • initpost 语句都是可选的

    go
    func main() {
    	sum := 1
    	for ; sum < 1000; {
    		sum += sum
    	}
    	fmt.Println(sum)
    }
    
  • for range 遍历字符串时按 rune(Unicode 码点)迭代,不是按字节

    go
    s := "你好, world"          // 中文字符占 3 个字节(UTF-8)
    for i, r := range s {
        fmt.Printf("%d: %c\n", i, r)
    }
    // 输出:
    // 0: 你
    // 3: 好        ← 注意索引跳到了 3,"你" 占 3 字节
    // 6: ,
    // 7: (空格)
    // 8: w
    // ...
    
    // 普通 for 循环(字节遍历):
    for i := 0; i < len(s); i++ {
        fmt.Printf("%d: %x\n", i, s[i])
    }
    // 输出每个字节的十六进制值,中文字符会被拆成多个字节
    

    💡 rune 是 Go 对 Unicode 码点 的术语(底层是 int32)。处理中文、emoji 等多字节字符时,用 for range 而非 for i < len(s)

  • ++--语句而非表达式,所以不能写成 j = i++。Go 没有逗号运算符,for 中多变量操作用平行赋值:for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {}

  • breakcontinue 可以带标签(label),用于跳出外层循环:

    go
    OuterLoop:
        for i := 0; i < 5; i++ {
            for j := 0; j < 5; j++ {
                if i*j > 10 {
                    fmt.Println("跳出外层循环")
                    break OuterLoop  // 跳出外层 for,而不是内层
                }
            }
        }
    
        // continue 也支持标签:
    NextRow:
        for i := 0; i < 3; i++ {
            for j := 0; j < 3; j++ {
                if j == 1 {
                    continue NextRow  // 跳过当前外层循环的剩余迭代
                }
            }
        }
    

    break 不带标签只能跳出最内层的 for/switch/select;带标签可以跳出任意外层。这在嵌套循环中非常实用。

switch

go
// switch(自动 break)
switch day {
case "Mon", "Tue":  // 多条件
    fmt.Println("工作日")
case "Sat", "Sun":
    fmt.Println("周末")
default:
    fmt.Println("其他")
}

// 类型选择(switch 特殊用法)
switch v := interfaceValue.(type) {
case int:
    fmt.Println("整数")
case string:
    fmt.Println("字符串")
}


go语言会自动加上 break

“The break statement that is needed at the end of each case in those languages is provided automatically in Go. Another important difference is that Go’s switch cases need not be constants, and the values involved need not be integers.”

go
import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
    // switch 不加条件表达式相当于 switch true.
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}
  • switch 适合每个条件表达式下的执行语句不多、简洁的情况,此时使用 switch 语句可读性好

4. 常用工具包/函数

Go 标准库提供了丰富的实用工具包,大多数日常开发需求无需第三方依赖。

strings — 字符串操作

go
import "strings"

s := "hello, world"

// 包含判断
strings.Contains(s, "world")       // true
strings.HasPrefix(s, "hello")      // true
strings.HasSuffix(s, "world")      // true

// 大小写转换
strings.ToUpper(s)                  // "HELLO, WORLD"
strings.ToLower(s)                  // "hello, world"

// 分割与拼接
strings.Split("a,b,c", ",")        // ["a" "b" "c"]
strings.Join([]string{"a", "b"}, "-") // "a-b"

// 替换
strings.ReplaceAll(s, "hello", "你好") // "你好, world"
strings.Replace(s, "l", "L", 2)       // "heLLo, world" (替换前2个)

// 去除空白
strings.TrimSpace("  hello  ")     // "hello"
strings.Trim("!!!hello!!!", "!")   // "hello"

// 统计与查找
strings.Count(s, "l")              // 3
strings.Index(s, "world")          // 7

strconv — 类型转换

go
import "strconv"

// 字符串 → 数值
i, err := strconv.Atoi("123")       // i=123
i64, _ := strconv.ParseInt("FF", 16, 64) // 十六进制解析, i64=255
f, _ := strconv.ParseFloat("3.14", 64)   // f=3.14
b, _ := strconv.ParseBool("true")        // b=true

// 数值 → 字符串
s := strconv.Itoa(456)              // "456"
s := strconv.FormatInt(255, 16)     // "ff"
s := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
s := strconv.FormatBool(true)       // "true"

fmt — 格式化输出

go
import "fmt"

name, age := "Alice", 30

// 打印
fmt.Print("hello")                  // 不换行
fmt.Println("hello")                // 换行
fmt.Printf("name=%s, age=%d\n", name, age)

// 格式化字符串(不打印,返回字符串)
s := fmt.Sprintf("name=%s, age=%d", name, age)

// 常用占位符
// %v  默认格式,万能占位符  →  "Alice" / 30 / [1 2 3]
// %+v 打印结构体时带字段名   →  {Name:Alice Age:30}
// %#v Go语法表示             →  main.Person{Name:"Alice", Age:30}
// %T  打印类型              →  string / int
// %d  整数                  →  30
// %s  字符串                →  "Alice"
// %f  浮点数                →  3.140000
// %.2f 保留两位小数          →  3.14
// %t  布尔                  →  true
// %p  指针地址              →  0xc000012345

time — 时间处理

go
import "time"

now := time.Now()                   // 当前时间
year, month, day := now.Date()      // 年/月/日
hour, min, sec := now.Clock()       // 时/分/秒

// 格式化(Go 使用特定的参考时间:2006-01-02 15:04:05)
now.Format("2006-01-02 15:04:05")   // "2026-07-03 20:00:00"
now.Format("2006/01/02")            // "2026/07/03"
now.Format("15:04")                 // "20:00"

// 解析
t, _ := time.Parse("2006-01-02", "2026-07-03")

// 时间计算
t1 := time.Now()
t2 := t1.Add(2 * time.Hour)         // 两小时后
dur := t2.Sub(t1)                   // 2h0m0s

// 定时器
timer := time.NewTimer(3 * time.Second)
<-timer.C                           // 3 秒后收到信号

ticker := time.NewTicker(1 * time.Second)  // 每秒触发
for t := range ticker.C {
    fmt.Println(t)
}

记忆:Go 的格式化时间参考值是 2006-01-02 15:04:05,对应 1 2 3 4 5 6 递增序列

math — 数学运算

go
import "math"

math.Abs(-5)            // 5
math.Max(3, 7)          // 7
math.Min(3, 7)          // 3
math.Pow(2, 10)         // 1024
math.Sqrt(16)           // 4
math.Ceil(3.14)         // 4(向上取整)
math.Floor(3.14)        // 3(向下取整)
math.Round(3.5)         // 4(四舍五入)

math.Pi                 // 3.141592653589793...

encoding/json — JSON 处理

go
import "encoding/json"

type Person struct {
    Name string `json:"name"`   // 用字段标签控制 JSON 键名
    Age  int    `json:"age"`
}

// 结构体 → JSON(序列化)
p := Person{Name: "Alice", Age: 30}
jsonBytes, _ := json.Marshal(p)         // {"name":"Alice","age":30}
jsonStr, _ := json.MarshalIndent(p, "", "  ") // 带缩进

// JSON → 结构体(反序列化)
jsonStr := `{"name":"Bob","age":25}`
var p2 Person
json.Unmarshal([]byte(jsonStr), &p2)    // Person{Name:"Bob", Age:25}

// 未知结构的 JSON → map
var result map[string]interface{}
json.Unmarshal([]byte(jsonStr), &result)

结构体标签(struct tag) 是 Go 的元编程机制,json:"name" 指定序列化时的 JSON 字段名。标签格式:`key:"value"`,多个标签用空格分隔。

os — 系统操作

go
import "os"

// 文件操作
data, err := os.ReadFile("data.txt")        // 一次性读取整个文件
os.WriteFile("output.txt", []byte("hello"), 0644) // 写入文件

// 文件打开(精细控制)
file, err := os.Open("data.txt")            // 只读打开
defer file.Close()

// 环境变量
home := os.Getenv("HOME")
os.Setenv("KEY", "value")

// 命令行参数
args := os.Args   // 程序启动参数,args[0] 是程序名

sort — 排序

go
import "sort"

// 基础类型排序
nums := []int{3, 1, 4, 1, 5}
sort.Ints(nums)                         // [1 1 3 4 5]
sort.Sort(sort.Reverse(sort.IntSlice(nums))) // 降序

strs := []string{"c", "a", "b"}
sort.Strings(strs)                      // ["a" "b" "c"]

// 自定义排序
type Person struct { Name string; Age int }
people := []Person{{"Bob", 30}, {"Alice", 25}}
sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age  // 按年龄升序
})

errors — 错误创建

go
import (
    "errors"
    "fmt"
)

err1 := errors.New("something went wrong")
err2 := fmt.Errorf("读取文件失败: %s", filename)

// Go 1.13+ 错误包装
err3 := fmt.Errorf("处理失败: %w", err1)
errors.Is(err3, err1)       // true — 判断是否包含该错误
errors.Unwrap(err3)         // err1 — 解包取原始错误

常用包速查表

包名 用途 常用函数
fmt 格式化输入输出 Println, Sprintf, Errorf
strings 字符串操作 Contains, Split, Join, ReplaceAll, Trim
strconv 字符串类型转换 Atoi, Itoa, ParseInt, FormatFloat
time 时间处理 Now, Format, Parse, Add, Sub
math 数学计算 Max, Min, Pow, Sqrt, Abs
os 系统操作 ReadFile, WriteFile, Getenv, Args
sort 排序 Ints, Strings, Slice
encoding/json JSON 序列化/反序列化 Marshal, Unmarshal
errors 错误处理 New, Is, As, Unwrap
sync 并发同步 Mutex, WaitGroup, Once
context 上下文控制 WithTimeout, WithCancel, WithValue
io 流式输入输出 ReadAll, Copy, WriteString

与 Java 对比

Go 的语法非常简洁,去掉了许多"样板代码",并且强制统一的代码风格

特性 Java Go (Golang) 核心差异点
代码组织 必须定义在 class 中。 直接定义在 package 下,无需类包裹。 Go 没有类的概念,只有包(文件)和函数/结构体。
变量声明 int age = 25; String name = "Go"; var age int = 25 name := "Go" (短变量声明) 类型后置:类型写在变量名后面。 类型推断:= 是 Go 中最常用的声明方式。
常量 public static final int MAX = 100; const Max = 100 Go 没有访问修饰符,通过首字母大小写控制导出(大写=公开,小写=私有)。
分号 语句末尾必须加分号 ; 不需要手动加分号 编译器会自动在行尾插入分号。
循环 for, while, do-while 三种。 只有 for Go 的 for 万能,配合 range 遍历集合。
条件判断 if (x > 0) { ... } if x > 0 { ... } 括号可选,支持初始化语句if x := f(); x > 0 {}
Switch 默认会穿透(需要 break 默认不穿透(自动 break 更安全,若需穿透需显式写 fallthrough。Case 可以是任意类型。
数组/切片 int[] arr = new int[5]; List<Integer> list arr := [5]int{} slice := []int{} Go 的切片是动态数组,底层是数组视图,性能更高且无装箱拆箱
类型转换 支持隐式转换(如 int→long) 必须显式转换 int64(num),无隐式转换减少意外错误。

代码对比示例:

java
// Java
public class Main {
    public static void main(String[] args) {
        int i = 0;
        while (i < 5) {
            if (i % 2 == 0) {
                System.out.println("Even: " + i);
            }
            i++;
        }
    }
}
// Go
package main

import "fmt"

func main() {
    i := 0
    for i < 5 {
        if i % 2 == 0 {
            fmt.Println("Even:", i)
        }
        i++
    }
    
    // Go 特有的 if 初始化
    if x := 10; x > 5 {
        fmt.Println("x is large")
    }
    // 注意:x 的作用域仅限于 if 块内
}

二、函数

1. 函数定义

func 函数名(入参) 返回值{

}

go
// 1. 基础函数示例
func Add(a int, b int) int{
    return a + b
}
// 相同类型入参的简化写法:
func Add(a, b int) int {
    return a + b
}

// 2. 多返回值函数示例(Go 特色:函数可以有任意数量的返回值)
func swap(x, y string) (string, string){
    return y, x;
}
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil
}

// 允许命名返回值
func GetInfo() (name string, age int) {
    name = "Alice"
    age = 30
    return  // 自动返回和返回值命名匹配的变量,可读性差,不推荐在长代码块函数里面yong(“A return statement without arguments returns the named return values. This is known as a "naked" return.“)
    
}

// 可变参数:能动态接收多个参数
func Sum(nums ...int) int {
    total := 0
    for _, val := range nums {
        total += val
    }
    return total
}
// 调用:Sum(1, 2, 3) 或 Sum(slice...)

函数名大写:表示当前函数是公有的,相当于 Java 的public关键字

函数名小写:表示当前函数是私有的,相当于 Java 的private关键字

2. init 函数与 import 导包

import 导包的几种写法

go
// 1. 单行导入
import "fmt"
import "os"

// 2. 分组导入(推荐)
import (
    "fmt"
    "os"
)

// 3. 别名导入 — 解决包名冲突,或简化长包名
import (
    myfmt "mylib/fmt"                     // 自定义别名
    cm "github.com/easierway/concurrent_map"
)

// 4. 匿名导入 — 只执行 init(),不直接使用包内函数
import _ "github.com/lib/pq"             // 常用于注册数据库驱动、插件等

// 5. 点导入 — 将包内所有公开符号导入当前命名空间(不推荐,可读性差)
import . "fmt"                           // 之后可直接写 Println() 而非 fmt.Println()

注意:Go 不允许导入未使用的包 (确保代码干净、无冗余依赖),否则编译报错。

包的初始化:init() 函数

Go 中有一种特殊的函数 init(),它不能被手动调用,由 Go 运行时在程序启动时自动执行。每个 .go 文件可以包含多个 init() 函数。

go
package main

import "fmt"

// init 函数:无参数,无返回值
func init() {
    fmt.Println("第一个 init")
}

func init() {
    fmt.Println("第二个 init")
}

func main() {
    fmt.Println("main 函数")
}

// 输出:
// 第一个 init
// 第二个 init
// main 函数

init 函数的执行顺序:

text
┌─────────────────────────────────────────────────┐
│  同一文件内:按 init() 声明顺序依次执行            │
│  同一包内:按 .go 文件名排序,依次执行各文件的 init │
│  跨包依赖:被导入的包的 init 先于当前包的 init      │
│  main 包的 init 执行完后,才执行 main() 函数       │
└─────────────────────────────────────────────────┘

示例:多包依赖下的 init 顺序

go
// ====== db/driver.go ======
package db

import "fmt"

func init() {
    fmt.Println("db/driver: init")
}
go
// ====== db/connection.go ======
package db

import "fmt"

func init() {
    fmt.Println("db/connection: init")
}
go
// ====== service/user.go ======
package service

import (
    "fmt"
    "myapp/db"            // service 依赖 db
)

func init() {
    fmt.Println("service/user: init")
}
go
// ====== main.go ======
package main

import (
    "fmt"
    "myapp/db"
    "myapp/service"
)

func init() {
    fmt.Println("main: init")
}

func main() {
    fmt.Println("main() 执行")
}
text
// 输出顺序:
// db/driver: init          ← 最底层的依赖先初始化
// db/connection: init      ← 同包内按文件名排序
// service/user: init       ← 依赖 db 的 service 后初始化
// main: init               ← main 包的 init 最后
// main() 执行              ← 最后执行 main()

init 函数的典型用途:

场景 示例
注册数据库驱动 import _ "github.com/go-sql-driver/mysql"
配置初始化 加载配置文件、设置日志级别
环境检查 校验必要的环境变量是否存在
注册到全局工厂模式 各插件包通过 init() 将自己注册到主程序的插件注册表中

注意事项:

  • init() 中不要执行耗时操作(影响启动速度)或可能 panic 的逻辑
  • 避免在 init() 中依赖其他包的 init() (顺序虽然确定,但耦合度高)
  • 一个包被多个包导入时,其 init()只执行一次

开发技巧:用空白标识符临时"使用"未引用的包/变量

Go 严格要求导入的包和声明的变量必须被使用,但开发过程中经常临时注释掉代码。这时可以用 _ 来"占位":

go
import (
    "fmt"
    _ "io"  // 如果注释掉了使用 io 的代码,这样可以让编译通过(但不推荐长期保留)
)

var _ = fmt.Printf   // 临时引用 fmt,编译通过

func main() {
    fd, _ := os.Open("test.go")
    _ = fd            // TODO: 后续会使用 fd
}

这些只是开发阶段的临时技巧,提交代码前应该清理干净。

关于循环依赖的详细说明和解决方式,见第五章 → 循环依赖

3. 匿名函数与闭包

匿名函数和闭包是 Go 函数式编程的核心工具,也是理解 defer、goroutine 等高级特性的基础

(1) 匿名函数(Anonymous Function)

匿名函数即没有名字的函数,可以像普通值一样被赋值、传参和返回(前面 Function values 已经提到过)

go
// 1. 定义并立即执行匿名函数(最常用)
func() {
    fmt.Println("Hello")
}() // ← 末尾的 () 表示立即调用

// 带参数 + 返回值
result := func(a, b int) int {
    return a + b
}(3, 4) // result = 7

// 2. 赋值给变量,后续调用(相当于正常定义了一个函数)
add := func(a, b int) int {
    return a + b
}
fmt.Println(add(1, 2)) // 3
fmt.Println(add(5, 6)) // 11

// 3. 作为另一个函数的参数(回调模式)
nums := []int{1, 2, 3, 4}
// sort.Slice 接收一个匿名函数来决定排序规则
sort.Slice(nums, func(i, j int) bool {
    return nums[i] > nums[j] // 降序
})

// 4. 作为函数的返回值(闭包,见下文)

匿名函数 + go 关键字 = goroutine

go func() { ... }() 是启动并发协程的常用写法。

(2) 闭包(Closure)

闭包 = 匿名函数 + 它捕获的外部变量闭包"记住"的不是外部变量的副本,而是变量的引用——即使外部函数已经返回,闭包仍然能访问和修改那个变量

go
// 闭包经典示例:累加器
func adder(initial int) func(int) int {
    sum := initial                         // sum 属于 adder 的局部变量
    return func(x int) int {               // 返回的匿名函数"捕获"了 sum
        sum += x                           // 修改的是同一个 sum
        return sum
    }
}

func main() {
    f := adder(0)   // 创建一个闭包,它有自己的 sum,初始值 = 0
    fmt.Println(f(1))  // sum = 0 + 1 = 1
    fmt.Println(f(2))  // sum = 1 + 2 = 3
    fmt.Println(f(3))  // sum = 3 + 3 = 6

    g := adder(100) // 再创建一个闭包,它有自己的 sum,初始值 = 100
    fmt.Println(g(1))  // sum = 100 + 1 = 101
    fmt.Println(g(2))  // sum = 101 + 2 = 103

    // f 和 g 的 sum 互不影响 — 每次调用 adder() 都会在堆上分配一个全新的 sum
    fmt.Println(f(1))  // sum = 6 + 1 = 7   (f 的 sum 还是 6)
}

图解闭包的"绑定"过程:

text
adder() 被调用
  │
  ├─ 在堆上分配 sum := 0sum 逃逸到堆(因为返回的闭包引用了它)
  │
  └─ 返回匿名函数 func(x int) int { sum += x; return sum }
       │                         ← 这个匿名函数"绑定"了堆上的 sum
       │
       ▼
  每次调用这个返回的函数,都是在读写同一个 sum

进阶示例:工厂函数

闭包可以携带配置,返回定制化的处理函数,这就是函数式编程中常见的"工厂"模式:

go
// 返回一个"加固定值"的函数
func makeAdder(n int) func(int) int {
    return func(x int) int { return x + n }
}

// 返回一个过滤器:只保留 >= threshold 的值
func filterAbove(threshold int) func(int) bool {
    return func(x int) bool { return x >= threshold }
}

func main() {
    add5 := makeAdder(5)
    fmt.Println(add5(10)) // 15

    pass := filterAbove(60)
    scores := []int{55, 80, 45, 90, 60}
    for _, s := range scores {
        if pass(s) {
            fmt.Println("及格:", s) // 80, 90, 60
        }
    }
}

(3) 常见陷阱:循环变量捕获

在循环中创建闭包时,闭包捕获的是循环变量的引用,而不是每次迭代的值:

go
// 错误写法:所有闭包共享同一个循环变量 i
var funcs []func() int
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() int {
        return i // 捕获的是变量 i 的引用,不是值!
    })
}
for _, f := range funcs {
    fmt.Println(f()) // 输出:3 3 3(不是 0 1 2)
}
// 原因:循环结束后 i=3,所有闭包读到的都是同一个 i

// 正确写法一:用局部变量"快照"当前值
for i := 0; i < 3; i++ {
    i := i                            // ← 重新声明一个同名新变量,作用域只在本次迭代
    funcs = append(funcs, func() int {
        return i                      // 捕获的是这次迭代独有的 i
    })
}
// 输出:0 1 2

// 正确写法二:通过参数传入
for i := 0; i < 3; i++ {
    funcs = append(funcs, func(n int) func() int {
        return func() int { return n }
    }(i)) // 立即执行外层函数,把 i 的值传给 n
}
// 输出:0 1 2

同样的陷阱也出现在 for range + goroutine 中:for _, v := range items { go func() { use(v) }() } 会出问题。解决方式同上:v := v 或通过参数传递。

与 Java 对比

方面 Java(Lambda) Go(匿名函数/闭包)
语法 (x) -> x + 1 func(x int) int { return x + 1 }
变量捕获 只能捕获 effectively final(不可修改)的局部变量 可以捕获并修改外部变量
本质 语法糖,编译后生成 invokedynamic 指令 真·函数值,可赋值、传参、返回
立即执行 ((Supplier) () -> "hi").get() func() { ... }() 直接调用
循环陷阱 同样存在:lambda 捕获循环变量引用 同上,解决方式都是局部快照

4. defer 机制

defer 语句会将一个函数的执行延迟到外围函数返回时才进行,常用于资源清理(文件、锁、连接)(类似 Java 中的 finally)

被延迟调用的函数,其参数会立即求值,但函数调用本身要等到外围函数返回时才会执行。

go
package main

import "fmt"

func main() {
	defer fmt.Print("world")

	fmt.Print("hello ")
}
// 输出:hello world
go
// Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.
func readFile() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()  // 函数返回前自动执行
    
    // 多个 defer 按 LIFO 顺序执行(后进先出)
    defer fmt.Println("最后执行")
    defer fmt.Println("中间执行")
    // 输出顺序:中间执行 → 最后执行
    
    // 读取文件逻辑...
    return nil
}

注意:

  • 参数在 defer 声明时立即求值

  • 多个 defer后进先出顺序执行

  • defer 在 return 之后被调用

    go
    func deferFunc() int {
      fmt.Println("defer func called...")
      return 0
    }
    func returnFunc() int {
      fmt.Println("return func called...")
    }
    
    func returnAndDefer() int {
      defer deferFunc()
      
      return returnFunc()
    }
    // 输出
    // return func called...
    // defer func called...
    

    return 不是一个原子操作,分三步:先计算并设置返回值;然后执行已注册的 defer;最后函数才真正结束 把结果返回给调用方。

    go
    func deferFunc(a *int) int {
    	*a = 10
    	return *a
    }
    
    func returnFunc(a *int) int {
    	*a = 20
    	return *a
    }
    
    func returnAndDefer() int {
    	a := 1
    	defer deferFunc(&a)
    	return returnFunc(&a)
    }
    

    结果:

    函数返回值是 20,但函数结束前局部变量 a 最后会被改成 10

    执行过程

    1. a := 1
    2. defer deferFunc(&a) 注册延迟调用
    3. return returnFunc(&a) 先执行 returnFunc
    4. returnFunca 改成 20,并返回 20
    5. 这时返回值已经确定为 20
    6. 真正返回前,执行 deferFunc(&a)
    7. deferFunca 改成 10
    8. 函数返回 20

    结果

    • 局部变量 a 最后是 10
    • 函数返回值是 20

    (return returnFunc(&a) 得到的返回结果已经先拷贝出去了,所以最终返回的仍然是 20,不是 10)

与 Java 对比

Go 的函数是一等公民,但在定义、返回值和错误处理上与 Java 有显著不同。

特性 Java Go (Golang) 核心差异点
定义位置 必须在类内部 可以在包级别直接定义 函数不依附于对象存在
多返回值 不支持(需返回对象、Map 或数组) 原生支持 经典模式:(结果, error)
命名返回值 不支持 支持 可在函数签名中给返回值命名,return 时可省略变量名
可变参数 void func(String... args) func func(args ...string) 语法相似,传递切片时 Go 需要 ... 展开
延迟执行 无内置关键字(需 try-finally) defer 关键字 defer 注册的函数在当前函数返回前执行(LIFO)
错误处理 try-catch-finally 异常机制 显式返回 error Go 没有 Exception。错误也是值,需手动 if err != nil 判断
函数重载 支持(同名不同参) 不支持 需改名(如 ParseInt, ParseFloat)或利用可变参数/接口

代码对比示例:

java
// Java: 模拟多返回值通常需要创建 DTO 类或使用 Map
public Result divide(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("Divide by zero");
    }
    return new Result(a / b, null);
}
// 调用
try {
    Result r = divide(10, 2);
} catch (ArithmeticException e) {
    // 处理
}
// Go: 原生多返回值 + 显式错误处理
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}

// 调用处:必须显式处理错误
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}

三、面向对象编程(OOP)

Go 没有传统意义上的"类",但可以通过结构体(struct)方法(method) 实现面向对象特性。

OOP 特征 Go 的实现方式 与传统 OOP(Java/C++)的区别
封装 结构体 + 首字母大小写(类名、方法、属性)控制可见性(大写=导出,小写=私有) public/private/protected 关键字,无类作用域
继承 结构体匿名嵌入(组合) extends,通过组合复用字段和方法,Go 没有父子类型层级
多态 接口隐式实现(Duck Typing) 无需 implements 声明,只要方法签名匹配即视为实现了接口
构造 NewXxx() 工厂函数(惯例) 无构造函数重载,结构体直接用字面量 Type{...} 初始化
方法 函数 + 接收者(func (t Type) Method() 方法定义在结构体外部,通过接收者参数绑定到类型
抽象 接口(Interface) 只有方法集合约束,无抽象类、无字段约束、无 abstract 关键字
静态成员 包级别变量/函数 static 关键字,直接在包内定义函数或变量即可

Go 面向对象的设计哲学:组合优于继承,接口隐式满足,简单胜于复杂。下面逐一展开

1. 结构体的定义

结构体是一系列属性的集合

go
package main

import "fmt"

type Vertex struct {
    X int
    Y int
    age int
}

func main() {
  	// 1. 结构体的初始化
    fmt.Println(Vertex{1, 2, 3})// 输出{1 2 3}
    fmt.Println(Vertex{1, 2})// 报错:too few values in struct literal of type Vertex
  
  	vertex := Vertex{X:1, Y:2}
    fmt.Println(vertex)// 输出{1 2 0}
    // 可以使用 Name: 语法仅列出部分字段来初始化(而且属性的顺序无关紧要)
  	  
  
    // 2. 结构体的范围访问
    v := Vertex{1, 2, 3}
    v.age = 10
    ftm.Println(v.age) // 输出10
    
    
  	// 指向该结构体的指针也可以直接访问它的属性值(* 可以省略)
    p := &v
    p.age = 12 
    // "To access the field X of a struct when we have the struct pointer p we could write (*p).X. However, that notation is cumbersome, so the language permits us instead to write just p.X, without the explicit dereference."
    
  	fmt.Println(v.age) // 输出12
}
  • 首字母大写:表示可公共访问;小写:当前包私有
go
// 结构体的值传递和引用传递

type User struct {
    Name string
    Age  int
}

// 值传递:修改的是副本,不影响原变量
func renameByValue(u User, newName string) {
    u.Name = newName
}

// 引用传递:通过指针修改原变量
func renameByRef(u *User, newName string) {
    u.Name = newName
}

func main() {
    user := User{Name: "张三", Age: 25}

    // 值传递 — 原结构体不会被修改
    renameByValue(user, "李四")
    fmt.Println(user.Name) // 输出:张三(未改变)

    // 引用传递(传指针)— 原结构体被修改
    renameByRef(&user, "李四")
    fmt.Println(user.Name) // 输出:李四(已改变)

    // 对比:值传递 vs 引用传递对性能的影响
    // 值传递 = 复制整个结构体,结构体越大开销越大
    // 引用传递 = 只传一个 8 字节的指针地址,无论结构体多大,开销恒定
}

结构体标签(Struct Tag)

标签是附加在结构体字段上的元数据,用反引号 ` 包裹,格式为 key:"value",多个标签用空格分隔。标准库和第三方库通过反射读取标签来实现序列化、校验、ORM 映射等功能。(有点类似 Java 里面的注解,都是给程序看的,不是业务逻辑本身)

某些标准库或第三方库约定会去读取这些标签,所以当你想让这些库按你的意图工作时,就需要在结构体字段上写 tag

例如

1. JSON 序列化

如果你用 encoding/json,可以写:

go
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

这样序列化出来就是:

json
{"name":"Alice","age":30}

如果不写 tag,默认通常会用字段名:

json
{"Name":"Alice","Age":30}
2. 数据库 / ORM

有些 ORM 或数据库映射库会看:

go
type User struct {
    Name string `db:"username"`
}

意思是:结构体字段 Name 对应数据库列 username

3. 参数校验

有些校验库会看:

go
type User struct {
    Name string `validate:"required"`
    Age  int    `validate:"min=1"`
}

表示这个字段必填、年龄最小值是多少

go
type User struct {
    ID       int    `json:"id" db:"user_id"`
    Name     string `json:"name" xml:"name"`
    Age      int    `json:"age"`
    Password string `json:"-"`                  // "-" 表示忽略该字段
    email    string `json:"email"`               // 小写字段不导出,json tag 无效
}

u := User{ID: 1, Name: "Alice", Age: 30}
data, _ := json.Marshal(u)
fmt.Println(string(data))  // {"id":1,"name":"Alice","age":30}(Password 被忽略,email 未导出)

常见标签用途:

标签用途 示例 说明
json json:"name,omitempty" 序列化字段名;- 忽略;omitempty 空值时省略
xml xml:"name,attr" XML 序列化
db db:"user_id" database/sql 等 ORM 的数据库列名映射
validate validate:"required,min=1" go-playground/validator 校验规则
form form:"username" gin 等框架的 HTTP 表单参数绑定
yaml yaml:"name" YAML 序列化

标签是编译时不会校验的字符串,写错了不会报错,只在运行时通过反射读取时才会发现问题。如需读写标签,参见反射章节field.Tag.Get()

2. 方法绑定

go
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// 值接收(复制副本)—— 可以用于读操作,无法修改原始值【相当于值传递】
func (p Person) FullName() string {
    return p.FirstName + " " + p.LastName
}

// 指针接收(可修改原值)—— 需要修改结构体字段时使用【相当于引用传递】
func (p *Person) SetAge(newAge int) {
    p.Age = newAge
}

func (p *Person) Birthday() {
    p.Age++  // 指针接收者,可以直接修改原值
}

// 使用示例
func main() {
    p := Person{FirstName: "张", LastName: "三", Age: 25}

    // 值接收:p 和指向 p 的指针都可以调用
    fmt.Println(p.FullName())   // "张 三"
    ptr := &p
    fmt.Println(ptr.FullName()) // "张 三"(Go 自动解引用,等价于 (*ptr).FullName())

    // 指针接收:只有可寻址的变量能调用
    p.SetAge(30)
    fmt.Println(p.Age)          // 30(原值已修改)

    p.Birthday()
    fmt.Println(p.Age)          // 31

    // ⚠️ 值接收者不会修改原值
    // 这里修改的只是副本,原 p 不变 — 详见上方「结构体的值传递和引用传递」
}

相当于类的方法不在类里面,而是通过 (接收者变量 类型) 的方式绑定到类型上。接收者变量名习惯用类型首字母小写(如 p Persons *Server

选择指南:

场景 选择 原因
需要修改原值 指针接收者 值接收者操作的是副本
结构体较大 指针接收者 避免复制整个结构体的开销
只读 / 小结构体 值接收者 简单安全,无副作用
实现接口的方法 保持一致 同一类型的方法接收者类型应统一
基本类型的别名类型 值接收者 本身很小,无需指针

Go 的自动转换:当你用 ptr.FullName() 时,Go 会自动将指针 ptr 解引用为 (*ptr).FullName();当你用 p.SetAge(30) 时,Go 会自动取地址 (&p).SetAge(30)

了解:方法可以定义在任意命名类型上(不只是结构体)。只要类型不是指针或接口,就可以绑定方法:

go
// 1. 基于基本类型的自定义类型
type Celsius float64

func (c Celsius) String() string {
   	return fmt.Sprintf("%.1f°C", c)
}

// 2. 基于切片的自定义类型 —— 可以绑定方法,甚至实现 io.Writer
type ByteSlice []byte

func (p *ByteSlice) Write(data []byte) (n int, err error) {
   	*p = append(*p, data...)
   	return len(data), nil
}
// 现在 *ByteSlice 实现了 io.Writer,可以配合 fmt.Fprintf 使用:
// var b ByteSlice
// fmt.Fprintf(&b, "hello %s", "world")

// 3. 基于函数的类型 —— 让函数实现接口(如 http.HandlerFunc)
type HandlerFunc func(w http.ResponseWriter, r *http.Request)

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   	f(w, r)  // 方法体就是调用函数自身
}

💡 这意味着 Go 中几乎任何类型都可以拥有方法和实现接口——int 可以格式化输出,slice 可以变成 io.Writer,函数可以变成 HTTP handler。这种灵活性是 Go 组合式设计的基石。

3. 组合代替继承

go
type Animal struct {
    Name string
}

func (a Animal) Speak() {
    fmt.Println("Animal speaks")
}

// 嵌入结构体(组合)—— Dog 获得了 Animal 的所有字段和方法
type Dog struct {
    Animal      // 匿名嵌入
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "旺财"}, Breed: "哈士奇"}

    // 1. 直接访问嵌入结构体的字段和方法(提升)
    fmt.Println(d.Name)    // "旺财"
    fmt.Println(d.Animal.Name) // "旺财"
    d.Speak()              // "Animal speaks"

    // 2. 方法重写 —— Dog 可以覆盖 Animal 的方法
    // func (d Dog) Speak() { fmt.Println(d.Name, "汪汪") }
    // d.Speak()  // 输出 "旺财 汪汪"(调用 Dog 自己的 Speak)
    // d.Animal.Speak()  // 仍可显式调用被嵌入类型的方法

    // 3. 多层级嵌入
    // type Puppy struct { Dog }   // Puppy 同时拥有 Animal 和 Dog 的字段/方法
}

Go 组合 vs Java 继承:

特性 Java 继承 Go 组合(嵌入)
关系 Dog is a Animal(父子关系) Dog has a Animal(包含关系)
类型系统 子类可以赋值给父类引用(向上转型) Dog 和 Animal 是不同类型,不能互赋
方法提升 @Override 全覆盖 可重写,也可显式调 d.Animal.Speak()
多继承 不支持(只能单继承) 可嵌入多个结构体
构造链 super() 调用父类构造 手动初始化嵌入字段

Go 的组合不是继承的语法糖,它不会建立类型层级。如果你想用 Dog 替代 Animal(比如作为函数参数),需要通过接口来实现多态——详见下一节。

结构体嵌入(组合)补充:方法与接口继承

嵌入结构体时,被嵌入类型的方法会被"提升"到外层,外层类型也自动实现被嵌入类型满足的接口:

go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }

// 如果 bufio.Reader 实现了 io.Reader,bufio.Writer 实现了 io.Writer
// 那么嵌入了两者的结构体自动同时实现这两个接口:
type ReadWriter struct {
    *bufio.Reader   // 提升 Read 方法 → 实现了 io.Reader
    *bufio.Writer   // 提升 Write 方法 → 实现了 io.Writer
}
// ReadWriter 自动实现了 io.Reader、io.Writer、io.ReadWriter 三个接口!

接口也可以嵌入io.ReadWriter 就是嵌入了 io.Readerio.Writer 两个接口,而不是重复声明 ReadWrite 方法。

嵌入的名字冲突

当嵌入的多个类型有同名方法/字段时:

  • 外层声明的方法/字段遮蔽内层同名项
  • 同一嵌套层级的同名项会引发编译错误(除非从不在类型定义之外被使用)

4. 接口(Interface)

接口是 Go 实现多态的核心机制。与 Java 不同,Go 采用隐式实现:只要一个类型的方法集覆盖了接口的所有方法,它就自动实现了该接口,无需显式声明 implements

接口值的内部表示:静态类型 vs 具体类型

每个接口变量内部由一个 (type, value) 元组表示:

text
┌──────────────────────────────────┐
│  接口变量: var s Speaker         │
│  ┌────────────┬────────────────┐ │
│  │ 具体类型    │ 具体值          │ │
│  │ (concrete  │ (concrete      │ │
│  │  type)     │  value)        │ │
│  └────────────┴────────────────┘ │
└──────────────────────────────────┘
概念 英文 含义 确定时机
静态类型 static type 变量声明时的类型,即接口类型本身(如 Speaker 编译时
具体类型 concrete type / dynamic type 接口变量实际持有的底层值的类型(如 Dog 运行时
go
var s Speaker          // 静态类型=Speaker,具体类型=nil,具体值=nil
s = Dog{Name: "旺财"}  // 静态类型=Speaker,具体类型=Dog,具体值=Dog{Name:"旺财"}
s = Cat{}              // 静态类型=Speaker,具体类型=Cat,具体值=Cat{}

// 类型断言的本质:从接口中提取具体类型
dog, ok := s.(Dog)     // 检查 s 的具体类型是不是 Dog

类比:接口变量就像信封,信封类型(Speaker)永远不变,但信封里装的内容(Dog / Cat)可以动态替换。类型断言就是拆开信封看里面是什么。

(1) 接口定义与隐式实现

go
// 定义接口:声明一组方法签名
type Speaker interface {
    Speak() string
}

// Dog 实现了 Speaker(隐式,无需写 "implements Speaker")
type Dog struct{ Name string }
func (d Dog) Speak() string { return d.Name + ": 汪汪" }

// Cat 也实现了 Speaker
type Cat struct{}
func (c Cat) Speak() string { return "喵喵" }

func main() {
    // 多态:用接口类型接收不同的具体实现
    var s Speaker
    s = Dog{Name: "旺财"}
    fmt.Println(s.Speak())  // "旺财: 汪汪"
    s = Cat{}
    fmt.Println(s.Speak())  // "喵喵"
}

不过如果某个结构体没有完全实现接口的所有方法,接口类型就不能接收该结构体的实例

接口实现规则(重要):

接收者类型 接口变量能持有的值
1. 值接收者 T*T 都满足接口
2. 指针接收者 只有 *T 满足接口,T 不满足
  1. 值接收者
go
type Greeter interface { Greet() string }

type User struct{ Name string }

func (u User) Greet() string { return "Hi, " + u.Name }     // 值接收者

func main() {
    var g Greeter
    g = User{Name: "Alice"}  // 实现类实例的值可以
    g = &User{Name: "Bob"}   // 实现类实例的指针也可以(Go 会自动解引用)
}
  1. 指针接收者
go
func (u *User) SetName(name string) { u.Name = name }       // 指针接收者

func main() {
    var s Setter
    s = &User{Name: "Alice"}  // 实现类实例的指针可以
    s = User{Name: "Alice"}   // 实现类实例的值可以(会编译错误)
}

💡 记忆口诀:指针接收者的方法,只能通过指针调用;值接收者的方法,值和指针都能调用。

(2) 万能类型:interface{} / any

空接口没有任何方法,因此所有类型都实现了它,类似 Java 的 Object

go
// Go 1.18+ 推荐用 any( interface{} 的别名)
func PrintAny(v any) {
    fmt.Println(v)
}

PrintAny(42)            // int
PrintAny("hello")       // string
PrintAny(Dog{Name: "旺财"}) // struct

(3) 类型断言与类型选择

Go 提供了类型断言的机制,能判断接口变量的具体类型:

go
var v any = "hello"

// 类型断言(安全写法)
s, ok := v.(string) // 判断是否是 string 类型
if ok {
    fmt.Println("是字符串:", s)
}

i, ok := v.(int)
fmt.Println(i, ok)  // 0 false(断言失败返回零值和 false)

// 类型选择(配合 switch)
switch val := v.(type) { // 使用 .(type) 直接获取类型
    case int:
        fmt.Println("整数:", val)
    case string:
        fmt.Println("字符串:", val)
    case Dog:
        fmt.Println("狗叫:", val.Speak())
    default:
        fmt.Println("未知类型")
}

(4) 常用内置接口

Go 标准库定义了一些轻量接口,实现它们即可融入整个生态:

接口 方法签名 作用 实现示例
fmt.Stringer String() string 控制 fmt.Print 的输出格式 自定义类型的打印
error Error() string 表示错误值 errors.New()、自定义错误类型
io.Reader Read([]byte) (int, error) 读取数据流 文件、网络连接、strings.Reader
io.Writer Write([]byte) (int, error) 写入数据流 文件、网络连接、bytes.Buffer
sort.Interface Len() / Less() / Swap() 自定义排序 任意切片的排序

(5a) 接口设计原则

text
✅ 接受接口,返回结构体
✅ 接口要小(1-3 个方法最佳),按需定义
✅ 在使用方定义接口,而非实现方
❌ 避免大而全的接口(Java 式的 "AnimalInterface"
go
// 好:在使用方定义需要的接口
func Shout(s Speaker) {
    fmt.Println(strings.ToUpper(s.Speak()))
}

// 差:在实现方预先定义巨型接口
type AnimalInterface interface {
    Speak() string; Eat(); Sleep(); Run(); ...
}

(5b) 编译时接口检查

Go 的接口是隐式实现的,有时候没有静态转换让编译器帮你验证"某个类型确实实现了某个接口"。可以用空白标识符做编译时断言:

go
// 编译时断言:*RawMessage 必须实现 json.Marshaler 接口
var _ json.Marshaler = (*RawMessage)(nil)

这行代码不会创建任何变量(_ 丢弃了值),但如果 *RawMessage 没有实现 json.Marshaler编译就会报错。这种写法常用于库的内部实现中,确保接口契约不因重构而破坏。

(5c) 构造函数返回接口类型

如果一个类型只是为了实现某接口而存在,且不会暴露给其他包,那就只需要暴露接口,私有化具体类型:

go
// ===== 定义一个接口 =====
type Notifier interface {
    Notify(msg string) error
}

// ===== 具体实现:类型名小写,不公用 =====
type emailNotifier struct {
    address string
}

func (e *emailNotifier) Notify(msg string) error {
    // 发送邮件...
    fmt.Printf("向 %s 发送邮件:%s\n", e.address, msg)
    return nil
}

type smsNotifier struct {
    phone string
}

func (s *smsNotifier) Notify(msg string) error {
    // 发送短信...
    fmt.Printf("向 %s 发送短信:%s\n", s.phone, msg)
    return nil
}

// ===== 构造函数:返回接口,不暴露具体类型 =====
func NewEmailNotifier(address string) Notifier {
    return &emailNotifier{address: address} // 返回的是接口类型!
}

func NewSMSNotifier(phone string) Notifier {
    return &smsNotifier{phone: phone}
}

使用方这样调用:

go
// 调用方只依赖 Notifier 接口,不关心底层是邮件还是短信
func SendAlert(n Notifier) {
    n.Notify("服务器负载过高!")
}

func main() {
    email := NewEmailNotifier("admin@example.com")
    sms := NewSMSNotifier("13800138000")

    SendAlert(email) // 底层发邮件,调用方不知道也不关心
    SendAlert(sms)   // 换成短信,调用方代码一行都不用改
}

好处:

  • emailNotifiersmsNotifier 都是小写,包外完全不可见,无法被外部直接创建
  • 调用方只依赖 Notifier 接口,要替换实现只需改构造函数调用,其余代码不变
  • 相当于把"依赖倒置原则"做在了语言层面

原则接受接口,返回结构体 是默认规则;当类型的存在意义就是实现某个接口时,返回接口来隐藏实现是合理的例外。

5. 反射(了解)

反射让你在运行时检查和操作未知类型的变量。当你不确定传入的数据是什么结构时(比如写框架、ORM、序列化库),就需要用到它。

类比 Java 中的反射操作: Class.forName() + getDeclaredFields()

注意:反射代码可读性和性能都不如静态代码,能用接口解决的优先用接口。Go 标准库中 encoding/jsonfmt 等大量依赖反射,但普通业务代码应该慎用。

核心入口:reflect.TypeOf()reflect.ValueOf()

go
import "reflect"

x := 42
t := reflect.TypeOf(x)    // reflect.Type  — 类型信息(相当于 Java 的 Class<?>)
v := reflect.ValueOf(x)   // reflect.Value — 值信息(可读可写)

fmt.Println(t.Kind())     // "int"(底层种类)
fmt.Println(v.Int())      // 42(获取具体值)
fmt.Println(v.Interface())// 42(还原为 interface{})

常用操作

1. 遍历结构体字段(读取 struct tag)

go
type User struct {
    Name string `json:"name" db:"username"`
    Age  int    `json:"age"`
}

u := User{Name: "Alice", Age: 30}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    fmt.Printf("字段: %s, 类型: %s, 值:, json tag: %s\n",
               field.Name, field.Type, v.Field(i).Interface(), field.Tag.Get("json"))
}
// 输出:
// 字段: Name, 类型: string, json tag: name
// 字段: Age, 类型: int, json tag: age

2. 获取和修改字段值

go
v := reflect.ValueOf(&u).Elem()    // 必须传指针 + Elem() 才能修改

nameField := v.FieldByName("Name")
if nameField.CanSet() {
    nameField.SetString("Bob")     // u.Name = "Bob"
}

ageField := v.FieldByName("Age")
ageField.SetInt(25)                // u.Age = 25

fmt.Println(u)  // {Bob 25}

3. 动态调用方法

go
type Calculator struct{}

func (c Calculator) Add(a, b int) int { return a + b }

func main() {
    c := Calculator{}
    v := reflect.ValueOf(c)
    method := v.MethodByName("Add")

    args := []reflect.Value{
        reflect.ValueOf(3),
        reflect.ValueOf(4),
    }
    results := method.Call(args)
    fmt.Println(results[0].Int())  // 7
}

常用 API 速查

函数 / 方法 作用
reflect.TypeOf(v) 获取类型信息
reflect.ValueOf(v) 获取值信息
t.Kind() 底层种类(Int, String, Struct, Ptr…)
t.NumField() / t.Field(i) 遍历结构体字段
t.Field(i).Tag.Get("json") 读取 struct tag
v.Elem() 解引用指针,拿到指向的实际值
v.FieldByName("X") 按名字获取字段值
v.SetInt(n) / v.SetString(s) 修改值(需 CanSet()
v.MethodByName("X").Call(args) 按名字调用方法
v.Interface() reflect.Value 还原为 interface{}

与 Java 对比

概念层面的差异参见本章开头的概览表,下面通过一段等价代码直观感受两种语言的组织方式差异。

Java 方式:

java
public class Animal {
    private String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void Speak() {
        System.out.println("Animal speaks");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void Speak() {
        System.out.println("Woof!");
    }
}

Go 方式:

go
type Animal struct {
    Name string
}

func (a Animal) Speak() {
    fmt.Println("Animal speaks")
}

func NewAnimal(name string) *Animal {
    return &Animal{Name: name}
}

type Dog struct {
    Animal  // 嵌入
}

func (d Dog) Speak() {
    fmt.Println("Woof!")
}

type Speaker interface {
    Speak()
}
// var s Speaker = Dog{} // 合法,隐式实现

同样实现 Animal/Dog/Speak,Java 用了 class + extends + @Override,Go 用了 struct + 嵌入 + 方法绑定

四、错误处理机制

Go 不使用异常,而是通过返回 error 类型来处理错误。这是 Go 与 Java 最显著的差异之一

1. 错误类型基础

go
// error 是内置接口
type error interface {
    Error() string
}

// 标准库错误
errors.New("错误信息")
fmt.Errorf("格式化错误:%s", msg)

2. 自定义错误类型

go
type TimeoutError struct {
    Operation string
    Duration  time.Duration
}

// 实现 error 接口
func (e *TimeoutError) Error() string {
    return fmt.Sprintf("操作 '%s' 超时:%v", e.Operation, e.Duration)
}

// 创建自定义错误
err := &TimeoutError{Operation: "DB.Query", Duration: 5 * time.Second}

3. 错误判断与处理

go
// 基础错误检查
result, err := PerformOperation()
if err != nil {
    log.Printf("错误: %v", err)
    return
}

// 类型断言判断具体错误类型
if te, ok := err.(*TimeoutError); ok {
    log.Printf("超时操作:%s, 时长:%v", te.Operation, te.Duration)
}

// Go 1.13+ 推荐方式:errors.Is() 和 errors.As()
if errors.Is(err, os.ErrNotExist) {
    // 处理文件不存在
}

var te *TimeoutError
if errors.As(err, &te) {
    // 处理超时错误
}

4. 错误包装(Error Wrapping)

go
// 包装错误,保留原始错误链
err := doSomething()
if err != nil {
    return fmt.Errorf("处理失败: %w", err)  // %w 保留原始错误
}

// 解包错误
originalErr := errors.Unwrap(wrappedErr)

5. panic 与 recover(类似 try-catch)

go
// 抛出 panic(慎用)
panic("严重错误")

// 捕获 panic
func safeFunc() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("捕获 panic: %v", r)
        }
    }()
    // 可能 panic 的代码
}

最佳实践

  • 业务逻辑错误用 error 返回
  • panic 仅用于不可恢复的错误(如配置错误、数据不一致)
  • 优先使用 errors.Is()errors.As() 进行错误判断

与 Java 异常处理对比

特性 Java Go (Golang) 核心差异点
错误类型 Exception 类层次结构 error 接口 Go 的错误是值,不是控制流
抛出方式 throw new Exception() return errors.New() Go 错误作为返回值显式传递
捕获方式 try-catch-finally if err != nil Go 需手动检查每个错误
受检异常 支持(必须捕获) 不支持 Go 所有错误都是"非受检"的
错误链 getCause() errors.Unwrap() Go 1.13+ 支持错误包装
finally finally defer defer 更简洁,但功能略有不同
异常传播 自动向上抛 手动返回错误 Go 需要逐层返回错误

代码对比示例:

java
// Java
public String readFile(String path) throws IOException {
    try {
        return Files.readString(Paths.get(path));
    } catch (FileNotFoundException e) {
        throw new CustomException("文件不存在", e);
    } catch (IOException e) {
        throw new CustomException("IO 错误", e);
    }
}
// Go
func readFile(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return "", fmt.Errorf("文件不存在:%w", err)
        }
        return "", fmt.Errorf("读取失败:%w", err)
    }
    return string(data), nil
}

五、包与依赖管理

Go 的依赖管理经历了两个阶段:早期的 GOPATH 模式和现代的 Go Modules 模式。

1. GOPATH 模式(Go 1.10 及之前)

GOPATH 是 Go 最早的依赖管理方式

工作原理

GOPATH 是一个环境变量,指向你的"工作区"(workspace)。所有 Go 代码和依赖都必须放在 $GOPATH 下面:

text
$GOPATH/
├── src/          ← 所有源码(你的项目 + 第三方库)
│   ├── github.com/
│   │   └── gin-gonic/
│   │       └── gin/       ← go get 拉下来的库源码
│   └── myproject/         ← 你的项目
├── pkg/           ← 编译后的 .a 中间文件
└── bin/           ← go install 编译出的可执行文件

核心规则:你的项目代码必须放在 $GOPATH/src/ 目录下才能正常编译和引用第三方包。Go 编译器查找依赖时,直接去 $GOPATH/src/$GOROOT/src/(标准库)下找。

依赖管理方式:go get

bash
go get github.com/gin-gonic/gin   # 拉取最新代码到 $GOPATH/src/
go get -u github.com/gin-gonic/gin  # 更新到最新

go get 做的事情很简单:把远程仓库的源码 clone 到 $GOPATH/src/ 下。没有版本号、没有锁文件、没有语义化版本,你拿到的一直是远程仓库的最新 commit。

GOPATH 的三大痛点

痛点 说明
无版本控制 go get 永远拉最新代码,今天的代码和昨天可能不一样。不同机器上编译同一个项目,可能因为依赖版本不同而行为不一致
必须放在 GOPATH 下 项目必须放在 $GOPATH/src/ 里,不能用自己习惯的目录。Git 仓库也要在 src/ 下,非常别扭
单一工作区 所有项目共享同一个 $GOPATH,如果项目 A 需要 gin v1.3、项目 B 需要 gin v1.9,完全没法共存

社区尝试过各种补救方案:depglidegovendor……但都治标不治本。最终 Go 团队在 2018 年(Go 1.11)推出了官方的 Go Modules

2. Go Modules 模式(Go 1.13 起默认启用)

什么是 Go Module?

Go Module 是一组相关 Go 包的集合,以一个 go.mod 文件作为根。它解决了 GOPATH 的三个核心痛点:

text
GOPATH 时代                     Go Modules 时代
──────────────────────────────────────────────────
项目必须在 $GOPATH/src/      →  项目放在任意目录都行
依赖无版本,用最新 commit    →  语义化版本(v1.2.3)
所有项目共享一份依赖          →  每个项目独立管理依赖

类比 Java 生态:

  • go.modpom.xml / build.gradle声明依赖及版本
  • go.sumpom.xml<dependencyManagement> + 校验和(锁定版本 + 完整性校验
  • $GOPATH/pkg/mod/~/.m2/repository/本地缓存

Module 和 Package 的关系:一个 Module 可以包含多个 Package,每个子目录对应一个 Package。Module 是发布和版本管理的单位,Package 是代码组织的单位

go.mod 文件详解

go
// go.mod
module github.com/mycompany/myproject   // ← 模块路径(模块唯一标识,相当于 Maven 的 groupId:artifactId)

go 1.21                                  // ← 最低 Go 版本要求

require (
    github.com/gin-gonic/gin v1.9.0      // ← 直接依赖 + 版本号
    github.com/stretchr/testify v1.8.0
)

// 可选指令:
replace github.com/old/lib => github.com/new/lib v1.0.0   // 替换依赖源[相当于导入依赖时的重定向机制](用于 fork 或本地开发)
exclude github.com/broken/lib v1.2.3                       // 排除某个版本
retract v1.0.0                                             // 声明撤回已发布的版本

go.mod 只列直接依赖。间接依赖由 Go 自动管理,在 go mod tidy 时补全。

go.sum 文件

go.sumgo.mod 的伴侣文件,记录每个依赖版本的密码学哈希(SHA-256),用于:

  1. 完整性校验:下载的包是否被篡改
  2. 安全审计:每次构建都验证依赖的哈希是否匹配
go
github.com/gin-gonic/gin v1.9.0 h1:O3FBK4y9HhG9...= // 对‘github.com/gin-gonic/gin v1.9.0’下面所有文件做哈希
github.com/gin-gonic/gin v1.9.0/go.mod h1:x0K15p8H5... // 只对‘github.com/gin-gonic/gin v1.9.0’下面的 go.mod 文件做了哈希

版本选择:MVS(Minimal Version Selection)

Go 不选"最新版本",而是选"所有依赖都兼容的最低版本"——这叫 MVS(最小版本选择)

text
场景:
  你的项目 → A v1.5.0
  你的项目 → B v1.2.0
  A v1.5.0B v1.8.0A 需要更高版本的 B

MVS 结果:
  选 B v1.8.0(因为 A 要求 ≥ 1.81.2 不满足)

这与 npm 的"选最新版本"不同。MVS 的好处是确定性:相同 go.mod 永远得出相同的依赖版本,构建结果可复现

模块代理

Go 团队维护了官方模块代理 proxy.golang.org,你的依赖不直接从 GitHub 拉,而是通过代理:

text
go get → proxy.golang.org → GitHub(原始仓库)
          ↑
     缓存 + 加速 + 防仓库删除

这解决了"上游仓库删库跑路"的问题——代理会永久缓存已发布的版本。

3. 常用命令

bash
# 初始化模块(在项目根目录执行)【常用】
go mod init example.com/hello # 这里的"example.com/hello"新模块的唯一标识/路径【注意:如果你想发布这个 module,则它的唯一标识必须是一个可以被下载下来的路径(也就是它存放的代码仓库的路径)】

# 下载依赖(手动)
go get github.com/gin-gonic/gin          # 下载最新版本
go get github.com/gin-gonic/gin@v1.9.0   # 下载指定版本
go get -u github.com/gin-gonic/gin       # 升级到最新

# 以下这些命令会自动补充缺失的依赖
go build / go run / go test

# 整理依赖(添加缺失的,删除无用的)
go mod tidy

# 将依赖下载到本地缓存
go mod download

# 查看依赖树
go mod graph

# 查看"为什么依赖了这个包"
go mod why github.com/some/pkg

# 验证依赖完整性(比对 go.sum 的哈希)
go mod verify

# 查看当前模块的所有依赖列表
go list -m all

依赖默认下载到 $GOPATH/pkg/mod/(可通过 go env GOMODCACHE 查看),所有项目共享,不会重复下载(类比 Java 的 ~/.m2/repository)。如需修改本地缓存路径,见下方环境变量 → GOMODCACHE

4. 环境变量

Go Modules 的行为由一组环境变量控制,理解它们能帮你处理代理配置、私有仓库、离线开发等实际场景。

核心环境变量一览

变量 默认值 作用
GO111MODULE on(Go 1.16+) 开关:on 启用 / off 关闭 / auto 自动检测(只要项目包含 go.mod 文件就启用)
GOPROXY https://proxy.golang.org,direct 模块代理地址,多个地址用逗号分隔;
配置之后 Go 在后续拉取依赖时能直接通过镜像站点来快速拉取
GOPRIVATE 私有模块路径模式(通配符),匹配到的模块跳过代理和校验
GOSUMDB sum.golang.org 值表示校验和的数据源,用于在拉取模块版本时,确保拉取到的模块版本数据未经过篡改
GONOSUMDB 跳过 sum.golang.org 校验的模块路径模式
GONOPROXY 跳过代理直连的模块路径模式
GOMODCACHE $GOPATH/pkg/mod 模块本地缓存目录
GOINSECURE 允许通过 HTTP(非 HTTPS)拉取的模块路径模式
GONOSUMCHECK 空(Go 1.22+) 跳过 go.sum 校验的模块路径模式
GOWORK 指定 go.work 文件路径(workspace 模式)

关键变量详解

GO111MODULE — 控制模块模式开关

bash
# 三个值:
go env -w GO111MODULE=on    # 强制启用 Go Modules(Go 1.16+ 默认)
go env -w GO111MODULE=off   # 强制回退 GOPATH 模式
go env -w GO111MODULE=auto  # 有 go.mod 就启用,否则用 GOPATH

Go 1.16 起默认值为 on,正常情况下不需要手动设置。

GOPROXY — 模块代理

bash
# 查看当前代理
go env GOPROXY
# https://proxy.golang.org,direct

# 常见配置:
go env -w GOPROXY=https://goproxy.cn,direct           # 国内代理(七牛云),下载更快
go env -w GOPROXY=https://goproxy.io,direct            # 另一个国内代理
go env -w GOPROXY=direct                                # 不走代理,直接从源仓库拉
go env -w GOPROXY=off                                   # 禁止下载任何模块
go env -w GOPROXY=file://${GOPATH}/pkg/mod/cache/download,direct  # 优先用本地缓存

direct 的含义:如果代理上没有这个模块,直接去源仓库(GitHub 等)拉取。多个代理地址按顺序尝试,第一个不可用就试下一个。

GOPRIVATE — 私有模块配置(最常用)

公司内部的私有仓库不走公共代理,也不上传校验和:

bash
# 单个域名
go env -w GOPRIVATE=gitlab.mycompany.com

# 通配符匹配
go env -w GOPRIVATE=*.corp.com,*.internal.net

# 指定通配符后,这些模块会:
#   1. 不走 GOPROXY(直接连源仓库)
#   2. 不校验 GONOSUMDB(sum.golang.org 上没有私有模块的哈希)
#   3. 等同于同时设置了 GONOPROXY + GONOSUMDB + GONOSUMCHECK 为该模式

配合 Git 认证:私有仓库通常需要配置 .netrc 或 SSH key,或者设置 git config --global url."git@gitlab.mycompany.com:".insteadOf "https://gitlab.mycompany.com/"

GOMODCACHE — 模块缓存位置

bash
go env GOMODCACHE
# /Users/xxx/go/pkg/mod

# 修改缓存目录(比如磁盘空间不够)
go env -w GOMODCACHE=/data/go-mod-cache

类比 Java 的 ~/.m2/repository。所有项目的依赖共享这个缓存,不会重复下载。

查看与设置环境变量

bash
# 查看所有 Go 环境变量
go env

# 查看单个
go env GOPROXY
go env GOPRIVATE

# 永久设置(写入 $HOME/.config/go/env,或 OS 特定路径)
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOPRIVATE=*.corp.com

# 仅在当前终端生效(临时覆盖)
export GOPROXY=direct

典型场景配置

场景 推荐配置
国内开发 GOPROXY=https://goproxy.cn,direct
公司内网 + 私有 GitLab GOPRIVATE=gitlab.mycompany.com
离线 / 内网开发 配合 go mod vendor + GOPROXY=off
调试模块下载问题 GOPROXY=direct 排除代理干扰

5. 包的基本规则

规则 说明
包名 与目录名一致,通常小写,简短
可见性 首字母大写 = 导出(公有),小写 = 私有
文件组织 同一目录下所有 .go 文件必须属于同一个包
main 包 可执行程序必须包含 package mainfunc main()
循环依赖 严格禁止,编译直接报错 import cycle not allowed
go
// mypkg/utils.go
package mypkg

func PublicFunc() {}    // 外部可访问(大写开头)
func privateFunc() {}   // 仅包内可用(小写开头)

6. 导入与别名

导入语法的各种写法(单行/分组/别名/匿名/点导入)和 init() 函数详解见第二章 → init 函数与 import 导包,此处不再重复。

循环依赖

Go 严格禁止包之间形成循环依赖(A import B, B import A),编译直接报错。

text
❌ 错误示例:
A 包 import BB 包 import A 包    ← 编译错误:import cycle not allowed

解决方式:

  • 提取公共接口/类型到第三个包(C 包),A 和 B 都依赖 C
  • 通过接口解耦:A 定义接口,B 实现接口,避免 B import A
  • 合并为一个包(如果耦合确实紧密)

与 Java 依赖管理对比

特性 Java (Maven/Gradle) Go (Go Modules) 核心差异点
配置文件 pom.xml / build.gradle go.mod + go.sum Go 配置更简洁
版本策略 选最新兼容版本 MVS(最小版本选择) 保证构建可复现
依赖下载 mvn install / gradle build go get / go mod tidy Go 无需构建工具
远程仓库 Maven Central / jcenter proxy.golang.org(官方代理) Go 代理防仓库删除
本地缓存 ~/.m2/repository $GOPATH/pkg/mod 都是全局缓存
传递依赖 自动解析 自动解析 两者都支持
多模块 父子 POM Go 1.18+ workspace Go workspace 设计更轻量
项目位置 任意目录 任意目录 Go Modules 时代不限位置,GOPATH 时代必须放 src/
循环依赖 Maven 编译不会报错(但可能运行时出问题) 编译报错 Go 强制单向依赖

代码对比示例:

java
// Java: Maven pom.xml 声明依赖
// <dependency>
//   <groupId>com.google.code.gson</groupId>
//   <artifactId>gson</artifactId>
//   <version>2.10.1</version>
// </dependency>

import com.google.gson.Gson;

public class Main {
    public static void main(String[] args) {
        Gson gson = new Gson();
        String json = gson.toJson(new int[]{1, 2, 3});
        System.out.println(json);  // [1,2,3]
    }
}
go
// Go: go.mod 声明依赖
// module myapp
// go 1.21
// require github.com/gin-gonic/gin v1.9.0

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run()
}

六、并发编程(Concurrency)

Go 的并发模型基于 CSP(Communicating Sequential Processes) 理论,核心是 goroutinechannel。这是 Go 最强大的特性之一

Go 的并发哲学:用通信来共享内存

传统多线程编程(Java/C++)的核心模式是共享内存 + 锁——多个线程访问同一块数据,用互斥锁保护。这种模式容易出错:忘记加锁 → 数据竞争,加锁顺序不对 → 死锁。

传统的做法:通过共享内存来通信

text
   线程 A ──→ 共享变量 ←── 线程 B
              ↑ 锁保护 ↑

Go 的做法:通过通信来共享内存

text
   协程 A ──→ channel ──→ 协程 B
              ↑ 数据通过管道传递,无需锁 ↑

“Don’t communicate by sharing memory; share memory by communicating.”

1. Goroutine(轻量级协程)

Goroutine 是 Go 中的并发执行单元,由 Go 运行时而非操作系统调度:

对比维度 Java 线程 Go Goroutine
创建者 JVM → OS 内核 Go 运行时(用户态)
初始栈大小 ~1 MB(固定) ~2 KB(可动态扩容)
切换成本 内核态切换(~1-10μs) 用户态切换(~0.2μs)
创建数量 数百~数千 轻松数万~数十万
线程池 需要手动管理(ExecutorService) 运行时自动调度,无需关心
通信方式 shared memory + lock channel + select

go 关键字:在任意函数调用前加上 go 就能启动一个 goroutine,无需定义线程类、无需实现 Runnable——这是语言级别的并发支持。

go
func doWork(id int) {
    fmt.Println("worker", id)
}

func main() {
    go doWork(1)  // 启动一个 goroutine,不阻塞 main
    go doWork(2)  // 再启动一个

    time.Sleep(time.Second)  // 等待 goroutine 执行完(实际开发用 WaitGroup 或 channel)
}
  • 所有 goroutine 都依赖于整个进程 ,main 函数结束会导致进程结束=>所有 goroutine 一起退出

Java 开发需要"申请线程 → 提交任务 → 等待结果 → 回收线程"。

Go 中只需 go func(),运行时自动完成调度和复用,能更多的把注意力放在协程间的数据流上。

2. GMP 调度模型

Go 的调度器采用 GMP 模型,将 goroutine 高效地映射到操作系统线程上,是整个并发系统的基础。

三大角色

角色 全称 本质 数量 职责
G Goroutine 协程任务 数万~数十万 封装一段待执行的代码 + 栈 + PC 寄存器
M Machine OS 线程 动态增减(上限 SetMaxThreads 真正执行 G 的载体,直接对接内核
P Processor 逻辑处理器 = GOMAXPROCS(默认 CPU 核数) 持有 G 的本地队列,是 M 和 G 之间的桥梁

绑定关系与调度流程

默认状态(每个 P 绑定一个 M)

  1. 创建 goroutine 时,G 优先放入当前 P 的本地队列
  2. 绑定的 M 从 P 的本地队列按轮转顺序取 G 执行
  3. 本地队列满了,放一半到全局队列

调度器的设计

1. 并行

Go 调度器不只是把 goroutine 来回切换着跑,而是通过设置多个 P,尽量让它们真正分散到多个 CPU 核上同时跑,从而提升吞吐量和执行效率

最大并行数由 P 的最大数量(GOMAXPROCS)决定:如果 GOMAXPROCS = 1 ,即使你开了 10000 个协程,同一时刻也只有 1 个在真正占 CPU 跑

2. Work Stealing 机制 和 全局 G 队列

  • 场景:M1 忙碌、M2 空闲 无任务需要处理
  • 此时 M2 会先尝试从其他 P 偷一半 G 来处理,如果其他队列也没有,就会从全局队列获取(需要加锁)

3. hand off 机制

  • 场景:G1 任务进入阻塞状态,为了充分利用 CPU,此时会新创建一个 M3,将 P 交接(hand off)给新的 M3
  • 此时 M1 没有绑定 P,会进入睡眠/销毁,如果 G1 阻塞结束还需要继续执行,就会进入其他队列中

4. 抢占式

每个 G 最多容许运行 10ms → 超出 10ms,新的 G 会抢占 CPU(原有的 G 放回队列等待)

与 Java 线程模型对比

维度 Java Go
调度层级 内核线程 1:1 用户线程 G(协程) : M(线程) = N:1,通过 P 解耦
上下文切换 内核态,每次 ~1-10μs 用户态,每次 ~0.2μs
阻塞处理 线程阻塞 → 浪费内核资源 G 阻塞 → M 继续执行其他 G
队列模型 单一全局阻塞队列 每个 P 有本地队列 + 全局队列 + work stealing
抢占机制 依赖 JVM safepoint Go 1.14+ 基于信号的异步抢占

Go 通过引入 P 这一层间接,实现了 M 和 G 的解耦——这是它能在数万 goroutine 下保持高效的关键。Java 的虚拟线程(Project Loom, Java 21+)借鉴了类似思路。

3. Channel(通信机制)

Channel 是 Go 实现 CSP 并发模型的核心——它是 goroutine 之间传递数据的管道。前面讲过,Go 的并发哲学是"通过通信来共享内存",channel 就是这个"通信"的载体。

Channel 的本质

可以把 channel 理解为一根类型安全的管道(类似于 Java 的 BlockingQueue,但它是语言内置的,不需要 import 任何包):

text
goroutine A                    goroutine B
    │                              │
    │  ch <- data                  │  data := <-ch
    │  (发送数据到管道)              │  (从管道接收数据)
    │                              │
    └──────────▶  channel  ◀───────┘
                 (类型安全的数据管道)

关键特性:

  • 类型安全chan int 只能传 int,chan string 只能传 string
  • 并发安全:多个 goroutine 可以同时读写同一个 channel,Go 运行时保证原子性,无需加锁
  • FIFO 顺序:先发送的数据先被接收(队列语义)

(1) 创建与基本使用

go
// ===== 声明与创建 =====
var ch chan int             // 声明,此时 ch == nil(nil channel 读写都会永久阻塞!)
ch = make(chan int)         // 无缓冲 channel

ch := make(chan int, 3)     // 有缓冲 channel,容量 3

// ===== 发送与接收 =====
ch <- 42                    // 发送:把 42 放入 channel
value := <-ch               // 接收:从 channel 取出一个值
value, ok := <-ch           // 接收 + 检查:ok=false 表示 channel 已关闭且已空

// ===== 关闭 =====
close(ch)                   // 关闭 channel(发送方关闭,接收方检测)

// ===== 遍历 =====
for v := range ch {         // 不断从 channel 接收,直到 channel 被 close
    fmt.Println(v)
}

(2) 无缓冲 vs 有缓冲 — 理解阻塞行为

发送和接收时的阻塞行为取决于 channel 的缓冲类型:

无缓冲 Channel(make(chan T))— 同步通信

无缓冲 channel 的发送和接收是同步发生的:发送方必须等接收方准备好(发送方发送完数据到 channel 之后,也会阻塞 等待接收方收到数据),接收方也必须等发送方发送(如果此时 channel 为空,就会阻塞 等待发送方放入数据)。

类比 4x100 接力赛

go
func main() {
    ch := make(chan int) // 无缓冲

    go func() {
        fmt.Println("准备接收...")
        v := <-ch                        // 阻塞,直到有人发送
        fmt.Println("收到:", v)
    }()

    time.Sleep(1 * time.Second)          // 等 goroutine 进入接收状态
    fmt.Println("发送 42...")
    ch <- 42                             // 有人接收了,发送成功
    fmt.Println("发送完成")
}

// 输出:
// 准备接收...
// 发送 42...
// 收到: 42
// 发送完成

💡 无缓冲 channel 天然提供了同步语义:发送方知道接收方已经拿到数据了,这本身就是一种"确认"机制(“我发出去了,而且你一定收到了”)。

有缓冲 Channel(make(chan T, n))— 异步通信

有缓冲 channel 内部有一个环形队列。发送方在缓冲区满之前不会阻塞;接收方在缓冲区空之前不会阻塞。

类比:缓冲区起到快递柜的作用

go
func main() {
    ch := make(chan int, 2) // 容量 2 的缓冲

    ch <- 1   // 不阻塞,缓冲区: [1]
    ch <- 2   // 不阻塞,缓冲区: [1, 2]
    // ch <- 3 // 会阻塞!缓冲区满了,必须等有人取走一个

    fmt.Println(<-ch) // 1,缓冲区: [2]
    fmt.Println(<-ch) // 2,缓冲区: []
    // fmt.Println(<-ch) // 会阻塞!缓冲区空了,必须等有人发送
}
text
时间线(有缓冲 channel):
  发送 1[1] → 不阻塞,直接返回
  发送 2[1,2] → 不阻塞,直接返回
  发送 3[1,2,?] → 缓冲区满,阻塞等待
                  → 接收方取走 13 写入成功 → [2,3]

选择指南:

场景 推荐 原因
需要"发完即确认收到" 无缓冲 同步语义,天然确认
生产者-消费者,速率不匹配 有缓冲 缓冲区吸收突发流量
信号通知(“任务做完了”) 无缓冲 或 chan struct{} 只需同步,不需传数据
限流 / 令牌桶 有缓冲 buffer 大小 = 并发上限

(3) 单向 Channel — 接口层的安全保障

在函数签名中使用单向 channel,让编译器帮你检查数据流向:

go
// 只能发送
func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // 发送方负责关闭
}

// 只能接收
func consumer(ch <-chan int) {
    for v := range ch {
        fmt.Println(v)
    }
    // ch <- 1  // 编译错误:cannot send to receive-only channel
}

func main() {
    ch := make(chan int, 3)
    go producer(ch) // 双向 channel 自动转为单向
    consumer(ch)
}

💡 单向 channel 就像 Java 中的 ? extends T / ? super T 通配符——在 API 层面约束数据流向,防止误用。实际使用的还是同一个双向 channel,只是函数看到的"视角"不同。

(4) range 遍历与 close()

go
// 典型模式:生产者-消费者
func producer(ch chan<- int) {
    for i := 1; i <= 5; i++ {
        ch <- i
    }
    close(ch)  // ← 发送完毕,关闭 channel。消费者靠这个退出 range 循环
}

func main() {
    ch := make(chan int)
    go producer(ch)
    
    for v := range ch {  // channel 关闭且为空时,range 自动退出
        fmt.Println(v)
    }
    fmt.Println("接收完毕")
}

// 输出:1 2 3 4 5 接收完毕
  • 关于关闭 channel
    • channel 不像文件一样经常需要关闭,当你确实没有任何需要发送的数据/你想显式地结束循环,才去关闭 channel
    • 关闭 channel 后,可以继续从 channel 接收数据、无法向它发送数据

以下场景没有使用 range 循环,如果写入完毕没有关闭 channel 就会出现死锁

go
package main

import "fmt"

func main() {
    c :=make(chan int)
    
    go func() {
        for i:=0; i<5; i++ {
            c <- i
        }
        
        // 这里如果写入完毕没有关闭,for 循环就会一直阻塞等待 channel 放入数据,死锁
        close(c)
    }()
    for {
        // ok 用于判断是否关闭了,如果关闭了,就不继续从 channel 中读取
        if data, ok := <-c; ok {
            fmt.Println("从 channel 中读取到了:"+data)
        }else{
            // 退出无限循环
            break
        }
    }
    fmt.Println("Main Finished")
    
}

(5) 典型应用场景

场景一:goroutine 间传递数据(最基础)

go
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2  // 处理任务,发送结果
    }
}

func main() {
    jobs := make(chan int, 5)
    results := make(chan int, 5)

    go worker(1, jobs, results)

    jobs <- 1
    jobs <- 2
    jobs <- 3
    close(jobs)

    fmt.Println(<-results) // 2
    fmt.Println(<-results) // 4
    fmt.Println(<-results) // 6
}

场景二:信号通知 / 同步等待

go
// 用 channel 实现"等待任务完成"的信号
func main() {
    done := make(chan bool)

    go func() {
        fmt.Println("执行耗时操作...")
        time.Sleep(2 * time.Second)
        done <- true // 发信号:我做完了
    }()

    <-done // 阻塞等待信号
    fmt.Println("任务完成,继续执行")
}

场景三:超时控制(配合 select)

go
func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "结果"
    }()

    select {
    case result := <-ch:
        fmt.Println("收到:", result)
    case <-time.After(1 * time.Second):
        fmt.Println("超时!")
    }
}
// 输出:超时!(因为 goroutine 需要 3 秒,而超时设了 1 秒)

(6) 常见陷阱

陷阱一:向已关闭的 channel 发送 → panic

go
ch := make(chan int)
close(ch)
ch <- 1  // panic: send on closed channel

陷阱二:死锁 — 无缓冲 channel 没有配对

go
func main() {
    ch := make(chan int)
    ch <- 1        // 阻塞!没有 goroutine 在接收
    fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!

陷阱三:nil channel 永久阻塞

go
var ch chan int  // nil channel
select {
case ch <- 1:   // 永远不会执行(nil channel 永远阻塞)
default:
    fmt.Println("走 default")
}
// nil channel 在 select 中很有用:可以用 nil 来"禁用"某个 case

陷阱四:range 不会自动退出 — 忘记 close

go
ch := make(chan int)
go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    // 忘了 close(ch)!
}()

for v := range ch {  // 死锁!永远等不到 close
    fmt.Println(v)
}

与 Java BlockingQueue 对比

维度 Java BlockingQueue Go Channel
类型安全 泛型 <T> chan T,编译时检查
创建方式 new LinkedBlockingQueue<>(cap) make(chan T, cap),内置语法
阻塞语义 put() / take() ch <- v / v := <-ch,统一运算符
关闭通知 无原生支持(需 poison pill 模式) close(ch) + v, ok := <-ch,语言内置
遍历 手动 while 循环 for v := range ch,自动处理关闭
选择多路 无原生支持,需手动 poll() select 语句,语言内置
并发安全 通过内部锁保证 Go 运行时保证,无锁设计(CSP 管道)

4. select 多路复用

单流程下一个 go 只能监控一个 channel 的状态,而 select 可以实现监控多个 channel 的状态

go
select {
case msg := <-ch1:
    // 如果 ch1 成功读到数据,则进行该 case 处理语句
    fmt.Println("收到 ch1:", msg)
case msg := <-ch2:
    // 如果 ch2 成功读到数据,则进行该 case 处理语句
    fmt.Println("收到 ch2:", msg)
case <-time.After(1 * time.Second):
    fmt.Println("超时")
default:
    fmt.Println("无数据,不阻塞")
}

如果多个 channel 都可接收到数据,select 会随机选择可用的 case

5. 同步原语(sync 包)

go
// WaitGroup - 等待多个 goroutine 完成
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Println("任务", id)
    }(i)
}
wg.Wait()

// Mutex - 互斥锁
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

// RWMutex - 读写锁
var rwmu sync.RWMutex
func read() {
    rwmu.RLock()
    defer rwmu.RUnlock()
    // 读操作
}
func write() {
    rwmu.Lock()
    defer rwmu.Unlock()
    // 写操作
}

// Once - 只执行一次
var once sync.Once
func initConfig() {
    once.Do(func() {
        // 初始化代码,只执行一次
    })
}

6. Context(上下文控制)

go
// 创建 context
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

// 在 goroutine 中使用
func doWork(ctx context.Context) {
    select {
    case <-ctx.Done():
        fmt.Println("任务取消")
        return
    case <-time.After(10 * time.Second):
        fmt.Println("任务完成")
    }
}

// 传递 value(慎用)
ctx = context.WithValue(ctx, "key", "value")

最佳实践

  • 优先使用 channel 进行通信,而非共享内存
  • 使用 context 控制 goroutine 生命周期
  • 避免 goroutine 泄漏(确保所有 goroutine 能退出)

与 Java 并发对比

特性 Java Go (Golang) 核心差异点
并发单元 Thread / ExecutorService Goroutine Go 协程更轻量,创建成本低
通信方式 共享内存 + 锁 Channel + sync Go 提倡"通过通信共享内存"
线程池 ThreadPoolExecutor Go 运行时自动调度 Go 无需手动管理线程池
同步原语 synchronized, Lock, Semaphore Mutex, WaitGroup, Channel Go 原语更简洁
超时控制 Future.get(timeout) context.WithTimeout Go 的 context 更灵活
并发安全 需手动保证 需手动保证 两者都需要开发者注意
调试难度 较复杂(线程 dump) 较复杂(goroutine dump) Go 提供 pprof 工具

代码对比示例:

java
// Java - 线程池 + Future
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Integer>> futures = new ArrayList<>();

for (int i = 0; i < 5; i++) {
    futures.add(executor.submit(() -> {
        return compute(i);
    }));
}

for (Future<Integer> f : futures) {
    System.out.println(f.get());
}
executor.shutdown();
// Go - Goroutine + Channel
results := make(chan int, 5)
for i := 0; i < 5; i++ {
    go func(id int) {
        results <- compute(id)
    }(i)
}

for i := 0; i < 5; i++ {
    fmt.Println(<-results)
}

七、测试与调试

1. 单元测试

文件命名以 _test.go 结尾,测试函数以 Test 开头:

go
// add_test.go
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("期望 5,实际 %d", result)
    }
}

// 表格驱动测试(推荐)
func TestAddTable(t *testing.T) {
    tests := []struct {
        a, b, expected int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }
    
    for _, tt := range tests {
        t.Run(fmt.Sprintf("%d+%d", tt.a, tt.b), func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("期望 %d,实际 %d", tt.expected, result)
            }
        })
    }
}

2. 性能测试(Benchmark)

go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

// 运行性能测试
// go test -bench=. -benchmem
参数 说明
-bench=. 运行所有 benchmark
-benchmem 显示内存分配统计
-count=5 运行 5 次取平均
-cpuprofile 生成 CPU 性能分析文件

3. 测试覆盖率

bash
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out  # 生成 HTML 报告

# 查看覆盖率
go test -cover

4. 测试辅助函数

go
// 测试辅助函数(以 T 或 B 为第一个参数)
func setupTest(t *testing.T) {
    t.Helper()  // 标记为辅助函数,错误显示调用者位置
    // 初始化代码
}

// 并行测试
func TestParallel(t *testing.T) {
    t.Parallel()  // 标记为可并行执行
    // 测试代码
}

5. 调试工具

工具 用途 命令
go test 运行测试 go test -v
go test -race 检测数据竞争 go test -race
pprof 性能分析 go tool pprof
delve 调试器 dlv debug
go vet 代码检查 go vet ./...
bash
# 检测数据竞争
go test -race ./...

# 生成性能分析
go test -cpuprofile=cpu.prof -memprofile=mem.prof

# 使用 pprof 分析
go tool pprof cpu.prof

与 Java 测试对比

特性 Java (JUnit) Go (testing 包) 核心差异点
测试框架 JUnit, TestNG 内置 testing Go 无需额外依赖
断言库 AssertJ, Hamcrest 标准库 + testify Go 常用第三方断言库
Mock Mockito gomock, testify/mock Go 需第三方库
性能测试 JMH 内置 Benchmark Go 更简单易用
覆盖率 JaCoCo 内置 cover Go 内置支持
并行测试 @Parallel t.Parallel() 两者都支持
表格测试 参数化测试 表格驱动测试 Go 更灵活

八、最佳实践

1. 代码规范

格式化

使用 go fmt(或 gofmt)统一代码风格。Go 采取了一种不寻常的方式:让机器处理大多数格式化问题。不需要花时间对齐结构体字段的注释。

text
缩进:使用 Tab(gofmt 默认)
行长度:Go 没有行长度限制,如果一行太长,换行并多缩进一个 Tab
括号:控制结构(if/for/switch)不需要括号包裹条件表达式

命名规范

《Effective Go》中对命名有明确的惯例,遵循这些约定能让你的代码更容易被其他 Go 程序员理解:

1. 包名(Package names)

  • 小写、单单词、简短byteshttpjson,不需要下划线或驼峰
  • 包名是导入时的默认访问前缀(如 bytes.Buffer),所以宁短勿长
  • 包名 = 源码目录的 base name:encoding/base64 目录下的包名是 base64(不是 encoding_base64
  • 避免重复bufio 包里的 Reader 命名成 Reader 而不用命名成 BufReader,因为使用者使用时用的 bufio.Reader,不需要特别在命名上区分
  • 构造函数命名:ring 包只有一个公开的类型 Ring,构造函数命名为 Newring.New()),而不是 NewRing

2. Getter 和 Setter

Go 不自动支持 getter/setter,但可以自己写。Getter 方法名不要加 Get 前缀

go
// 推荐:Getter 直接用字段名(首字母大写)
func (u *User) Owner() string { return u.owner }

// 不推荐:写为 GetOwner()
func (u *User) GetOwner() string { return u.owner }

// Setter 可以加 Set 前缀
func (u *User) SetOwner(name string) { u.owner = name }

3. 接口命名

  • 单方法接口:方法名 + -er 后缀 → ReaderWriterFormatterCloser
  • 多方法接口:用能描述其行为的名称 → ReadWriterReadWriteCloser
  • 如果你的类型实现了与知名接口相同语义的方法,用相同的名称和签名:字符串转换方法叫 String 而不是 ToString

4. 驼峰命名(MixedCaps)

Go 使用 MixedCapsmixedCaps(驼峰式),不使用下划线分隔单词

go
// 推荐
var maxLength int
func NewUser() *User {}

// 不推荐
var max_length int  

注释

  • 导出的函数/类型/常量必须有注释,且注释以声明名称开头
  • /* */ 块注释主要用于包注释或禁用大段代码
  • 出现在顶层声明之前(中间没有空行)的注释被视为 doc comment,会被 go doc 提取
go
// NewUser 创建一个指定 name 的 User 实例。
// 如果 name 为空,返回错误。
func NewUser(name string) (*User, error) { ... }

其他

  • 错误处理:每个 error 都必须检查,不要用 _ 忽略 error 返回值
  • 并发安全:文档中说明类型/函数是否并发安全

2. 推荐的项目结构

text
project/
├── cmd/              # 可执行程序
│   └── main/
│       └── main.go
├── internal/         # 私有包(外部不可导入)
│   └── pkg/
├── pkg/              # 公共包
├── api/              # API 定义
├── configs/          # 配置文件
├── scripts/          # 脚本文件
├── tests/            # 集成测试
├── go.mod
└── go.sum

九、总结

从 Java 到 Go 的思维转变:

# 转变 Java 思维 Go 思维
1 面向对象 类 + 继承 结构体 + 组合
2 错误处理 try-catch if err != nil
3 并发模型 线程 + 锁 goroutine + channel
4 依赖管理 Maven/Gradle go mod
5 接口实现 显式 implements 隐式满足
6 代码风格 灵活多样 go fmt 统一
7 泛型支持 成熟(Java 5+) 较新(Go 1.18+)
8 构建速度 较慢 极快

建议:

  1. 接受 Go 的设计哲学,不要试图用 Java 的思维写 Go 代码

  2. 多读标准库代码 - 学习 Go 的惯用写法

  3. 理解并发模型 - 这是 Go 的核心优势,值得深入学习

参考