Go 语言进阶笔记 — 面向 JS/TS 前端开发者

Go 语言进阶笔记 --- 面向 JS/TS 前端开发者

进阶部分:结构体、接口、并发编程

1. 结构体(Struct)

结构体是 Go 中最重要的数据组织方式,用于将多个字段组合成一个自定义类型。类似 JS/TS 中的 classinterface,但更轻量------没有继承,只有组合。

结构体定义

go 复制代码
package main

import "fmt"

// type 类型名 struct { 字段名 类型 }
type Teacher struct {
    Name    string
    Age     int
    Subject string
}
ts 复制代码
// TS 等效:interface 描述数据形状
interface Teacher {
    name: string
    age: number
    subject: string
}

// 或用 class(带行为)
class Teacher {
    name: string
    age: number
    subject: string
}

结构体初始化

go 复制代码
func main() {
    // 方式一:按字段名初始化(推荐,字段顺序无关)
    t1 := Teacher{
        Name:    "杜宽",
        Age:     18,
        Subject: "Go",
    }

    // 方式二:按顺序初始化(不推荐,字段顺序变化会出错)
    t2 := Teacher{"Dot", 20, "K8s"}

    // 方式三:先声明再赋值(零值初始化)
    var t3 Teacher
    t3.Name = "小明"
    t3.Age = 22
    t3.Subject = "Python"

    // 方式四:new 返回指针(*Teacher)
    t4 := new(Teacher)
    t4.Name = "小红"
    t4.Age = 19

    fmt.Println(t1, t2, t3, *t4)
}
ts 复制代码
// TS 等效
const t1: Teacher = { name: "杜宽", age: 18, subject: "Go" }

const t3 = {} as Teacher
t3.name = "小明"
t3.age = 22
对比项 Go JS/TS
定义类型 type T struct { ... } interface T { ... } / class T { ... }
实例化 T{Field: val} { field: val } / new T()
零值 字段自动有零值("" / 0 / false undefined
指针实例 new(T) 返回 *T 无对应,对象天生是引用

访问和修改字段

go 复制代码
t1 := Teacher{Name: "杜宽", Age: 18, Subject: "Go"}

// 用 . 访问字段
fmt.Println(t1.Name)    // 杜宽
fmt.Println(t1.Age)     // 18

// 修改字段
t1.Age = 25
fmt.Println(t1.Age)     // 25

// 指针访问字段:Go 自动解引用,不需要写 (*t4).Name
t4 := new(Teacher)
t4.Name = "小红"         // 等价于 (*t4).Name = "小红"
fmt.Println(t4.Name)    // 小红

结构体方法

Go 的方法是绑定在类型上的函数,通过"接收者"实现,类似 class 的成员方法:

go 复制代码
// 值接收者:方法内操作的是结构体的副本,不影响原值
func (t Teacher) Introduce() string {
    return fmt.Sprintf("我是%s,今年%d岁,教%s", t.Name, t.Age, t.Subject)
}

// 指针接收者:方法内修改会影响原结构体(推荐用于需要修改字段的方法)
func (t *Teacher) Birthday() {
    t.Age++
}

func main() {
    t := Teacher{Name: "杜宽", Age: 18, Subject: "Go"}

    fmt.Println(t.Introduce())  // 我是杜宽,今年18岁,教Go

    t.Birthday()
    fmt.Println(t.Age)          // 19(Age 被修改了)
}
ts 复制代码
// TS 等效
class Teacher {
    name: string
    age: number
    subject: string

    constructor(name: string, age: number, subject: string) {
        this.name = name
        this.age = age
        this.subject = subject
    }

    /** 不修改状态的方法 */
    introduce(): string {
        return `我是${this.name},今年${this.age}岁,教${this.subject}`
    }

    /** 修改状态的方法 */
    birthday(): void {
        this.age++
    }
}

值接收者 vs 指针接收者

场景 选择 原因
方法需要修改字段 指针接收者 *T 操作原结构体,不是副本
结构体很大 指针接收者 *T 避免每次调用都复制整个结构体
方法只读,结构体小 值接收者 T 安全,不会意外修改原值
同一类型的方法 保持一致 混用会让调用者困惑

结构体嵌套(组合)

Go 没有继承,用嵌套(组合)实现代码复用:

go 复制代码
type Address struct {
    City   string
    Street string
}

// 具名嵌套:通过字段名访问
type Student struct {
    Name    string
    Age     int
    Address Address  // 嵌套 Address 结构体
}

// 匿名嵌套(嵌入):Address 的字段被"提升"到 Employee,可直接访问
type Employee struct {
    Name    string
    Address          // 匿名嵌套,不写字段名
}

func main() {
    // 具名嵌套访问
    s := Student{
        Name: "小明",
        Age:  20,
        Address: Address{City: "北京", Street: "朝阳路"},
    }
    fmt.Println(s.Address.City)  // 北京(需要通过字段名访问)

    // 匿名嵌套访问(字段提升)
    e := Employee{
        Name:    "杜宽",
        Address: Address{City: "上海", Street: "南京路"},
    }
    fmt.Println(e.City)          // 上海(直接访问,无需 e.Address.City)
    fmt.Println(e.Address.City)  // 上海(两种方式都可以)
}
ts 复制代码
// TS 等效:用 extends 继承或 & 交叉类型
interface Address {
    city: string
    street: string
}

interface Student {
    name: string
    age: number
    address: Address  // 组合
}

// 交叉类型(类似匿名嵌套)
type Employee = { name: string } & Address

结构体标签(Tag)

标签是附加在字段上的元数据,最常用于控制 JSON 序列化:

go 复制代码
import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name     string `json:"name"`                  // JSON key 用小写
    Age      int    `json:"age"`
    Password string `json:"-"`                     // - 表示序列化时忽略此字段
    Email    string `json:"email,omitempty"`        // omitempty:零值时不输出该字段
    Phone    string `json:"phone,omitempty"`
}

func main() {
    u := User{
        Name:     "杜宽",
        Age:      18,
        Password: "secret123",
        Email:    "dukuan@example.com",
        // Phone 未赋值,零值为 ""
    }

    data, _ := json.Marshal(u)
    fmt.Println(string(data))
    // 输出:{"name":"杜宽","age":18,"email":"dukuan@example.com"}
    // Password 被忽略,Phone 因 omitempty 且为零值也被忽略
}
ts 复制代码
// TS 等效:用 class-transformer 或手动处理
class User {
    name: string
    age: number
    // @Exclude() --- 需要 class-transformer 装饰器
    password: string
    email?: string  // 可选字段,undefined 时 JSON.stringify 会忽略
}

// 原生 JSON.stringify 不支持字段重命名,需要手动处理或用库

常用 JSON Tag 选项

Tag 含义
json:"name" 指定 JSON key 名称
json:"-" 序列化/反序列化时忽略此字段
json:"name,omitempty" 零值时不输出(空字符串、0、false、nil 都算零值)
json:"name,string" 数字类型以字符串形式输出(前端 bigint 场景常用)

结构体与 JSON 反序列化

go 复制代码
func main() {
    jsonStr := `{"name":"Dot","age":20,"email":"dot@example.com"}`

    var u User
    err := json.Unmarshal([]byte(jsonStr), &u)  // 注意:传指针
    if err != nil {
        fmt.Println("解析失败:", err)
        return
    }
    fmt.Println(u.Name, u.Age, u.Email)  // Dot 20 dot@example.com
}
ts 复制代码
// TS 等效
const jsonStr = `{"name":"Dot","age":20,"email":"dot@example.com"}`
const u: User = JSON.parse(jsonStr)
console.log(u.name, u.age, u.email)

结构体是值类型

go 复制代码
t1 := Teacher{Name: "杜宽", Age: 18, Subject: "Go"}
t2 := t1        // 值拷贝!t2 是 t1 的完整副本
t2.Name = "Dot"

fmt.Println(t1.Name)  // 杜宽 --- t1 不受影响
fmt.Println(t2.Name)  // Dot

// 如果需要引用语义,用指针
t3 := &t1
t3.Name = "小明"
fmt.Println(t1.Name)  // 小明 --- t1 被修改了!
ts 复制代码
// TS 中 class 实例是引用类型
const t1 = new Teacher("杜宽", 18, "Go")
const t2 = t1        // 引用!t2 和 t1 指向同一对象
t2.name = "Dot"
console.log(t1.name) // Dot --- t1 也被改了!

// 深拷贝需要手动处理
const t3 = { ...t1 }  // 浅拷贝

Go 独有特性

  • 没有继承,只有组合:Go 刻意不支持继承,通过嵌套结构体实现代码复用
  • 方法和类型分离 :方法可以定义在类型所在包的任意文件中,不必写在同一个 struct 块里
  • 接口隐式实现 :结构体不需要显式声明 implements,只要实现了接口的所有方法就自动满足接口(鸭子类型)
go 复制代码
// 接口定义
type Greeter interface {
    Greet() string
}

// Teacher 实现了 Greet 方法,自动满足 Greeter 接口
// 不需要写 "implements Greeter"
func (t Teacher) Greet() string {
    return "你好,我是" + t.Name
}

// 可以直接把 Teacher 赋值给 Greeter 接口变量
var g Greeter = Teacher{Name: "杜宽", Age: 18, Subject: "Go"}
fmt.Println(g.Greet())  // 你好,我是杜宽
ts 复制代码
// TS 需要显式声明 implements
interface Greeter {
    greet(): string
}

class Teacher implements Greeter {  // 必须显式声明
    greet(): string {
        return `你好,我是${this.name}`
    }
}

易错点

go 复制代码
// ❌ 结构体字段首字母小写 = 包外不可访问(类似 private)
type teacher struct {
    name string  // 小写:包外不可访问
    Age  int     // 大写:包外可访问(exported)
}

// ✅ 对外暴露的结构体和字段首字母大写
type Teacher struct {
    Name string
    Age  int
}

// ❌ nil 指针调用方法会 panic
var t *Teacher
t.Birthday()  // panic: nil pointer dereference

// ✅ 使用前判空
if t != nil {
    t.Birthday()
}

Go 结构体 vs JS/TS class 核心差异总结

对比项 Go 结构体 JS/TS class
继承 ❌ 不支持,用嵌套组合 extends
接口实现 隐式(鸭子类型) 显式 implements
访问控制 首字母大/小写 public / private / protected
值/引用 值类型,赋值复制 引用类型,赋值共享
构造函数 无,用工厂函数约定 constructor()
this 接收者变量(如 t this
静态方法 包级别函数 static 方法

2. 接口(Interface)

接口是 Go 中实现多态和抽象的核心机制。与 TS 的 interface 不同,Go 接口是行为契约 ------只描述"能做什么",不描述"是什么"。最关键的特性是隐式实现:一个类型只要拥有接口要求的所有方法,就自动满足该接口,无需任何声明。

接口定义

go 复制代码
// type 接口名 interface { 方法签名列表 }
type Animal interface {
    Speak() string
    Move() string
}
ts 复制代码
// TS 等效:接口描述行为
interface Animal {
    speak(): string
    move(): string
}

核心差异 :TS 接口可以描述属性和方法;Go 接口只能描述方法,不能包含字段。

隐式实现(鸭子类型)

Go 不需要显式声明 implements,只要实现了接口的全部方法,就自动满足接口:

go 复制代码
package main

import "fmt"

type Animal interface {
    Speak() string
    Move()  string
}

type Dog struct {
    Name string
}

// Dog 实现了 Speak 和 Move,自动满足 Animal 接口
// 不需要写 "implements Animal"
func (d Dog) Speak() string {
    return d.Name + " 说:汪汪汪!"
}

func (d Dog) Move() string {
    return d.Name + " 在奔跑"
}

type Bird struct {
    Name string
}

func (b Bird) Speak() string {
    return b.Name + " 说:叽叽喳喳!"
}

func (b Bird) Move() string {
    return b.Name + " 在飞翔"
}

// 函数接受接口类型,Dog 和 Bird 都可以传入
func describe(a Animal) {
    fmt.Println(a.Speak())
    fmt.Println(a.Move())
}

func main() {
    dog := Dog{Name: "旺财"}
    bird := Bird{Name: "小鸟"}

    describe(dog)   // 旺财 说:汪汪汪! / 旺财 在奔跑
    describe(bird)  // 小鸟 说:叽叽喳喳! / 小鸟 在飞翔
}
ts 复制代码
// TS 等效:需要显式 implements
interface Animal {
    speak(): string
    move(): string
}

class Dog implements Animal {  // 必须显式声明
    constructor(public name: string) {}
    speak(): string { return `${this.name} 说:汪汪汪!` }
    move(): string { return `${this.name} 在奔跑` }
}

class Bird implements Animal {
    constructor(public name: string) {}
    speak(): string { return `${this.name} 说:叽叽喳喳!` }
    move(): string { return `${this.name} 在飞翔` }
}

function describe(a: Animal): void {
    console.log(a.speak())
    console.log(a.move())
}
对比项 Go TS
实现声明 隐式,无需 implements 显式 implements InterfaceName
接口内容 只有方法签名 方法 + 属性
满足条件 实现所有方法即可 必须显式声明 + 实现所有成员
多接口实现 自动满足所有匹配的接口 需逐一 implements A, B, C

接口变量与多态

接口变量可以持有任何实现了该接口的值,这是 Go 多态的基础:

go 复制代码
package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// printShapeInfo 接受 Shape 接口,Circle 和 Rectangle 都可以传入
func printShapeInfo(s Shape) {
    fmt.Printf("面积: %.2f, 周长: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    // 接口变量可以持有不同的具体类型
    var s Shape

    s = Circle{Radius: 5}
    printShapeInfo(s)  // 面积: 78.54, 周长: 31.42

    s = Rectangle{Width: 4, Height: 6}
    printShapeInfo(s)  // 面积: 24.00, 周长: 20.00

    // 切片中存放不同类型(多态集合)
    shapes := []Shape{
        Circle{Radius: 3},
        Rectangle{Width: 2, Height: 5},
        Circle{Radius: 1},
    }
    for _, shape := range shapes {
        printShapeInfo(shape)
    }
}
ts 复制代码
// TS 等效
interface Shape {
    area(): number
    perimeter(): number
}

class Circle implements Shape {
    constructor(public radius: number) {}
    area(): number { return Math.PI * this.radius ** 2 }
    perimeter(): number { return 2 * Math.PI * this.radius }
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) {}
    area(): number { return this.width * this.height }
    perimeter(): number { return 2 * (this.width + this.height) }
}

const shapes: Shape[] = [new Circle(3), new Rectangle(2, 5)]
shapes.forEach(s => console.log(s.area(), s.perimeter()))

接口组合

Go 接口可以嵌套组合,类似 TS 的 extends

go 复制代码
type Reader interface {
    Read(p []byte) (n int, err error)
}

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

// ReadWriter 组合了 Reader 和 Writer 的所有方法
type ReadWriter interface {
    Reader
    Writer
}

// 实现 ReadWriter 的类型必须同时实现 Read 和 Write
type File struct {
    name string
}

func (f File) Read(p []byte) (int, error) {
    // 读取文件逻辑...
    return 0, nil
}

func (f File) Write(p []byte) (int, error) {
    // 写入文件逻辑...
    return len(p), nil
}

func main() {
    var rw ReadWriter = File{name: "test.txt"}
    // rw 同时满足 Reader、Writer、ReadWriter 三个接口
    _ = rw
}
ts 复制代码
// TS 等效
interface Reader {
    read(p: Uint8Array): [number, Error | null]
}

interface Writer {
    write(p: Uint8Array): [number, Error | null]
}

// 接口继承组合
interface ReadWriter extends Reader, Writer {}

空接口(interface{} / any)

空接口没有任何方法要求,因此所有类型都满足空接口 ,类似 TS 的 any

go 复制代码
package main

import "fmt"

// interface{} 可以接受任意类型(Go 1.18+ 可以用 any 代替)
func printAnything(v interface{}) {
    fmt.Printf("值: %v, 类型: %T\n", v, v)
}

func main() {
    printAnything(42)           // 值: 42, 类型: int
    printAnything("hello")      // 值: hello, 类型: string
    printAnything(true)         // 值: true, 类型: bool
    printAnything([]int{1, 2})  // 值: [1 2], 类型: []int

    // 空接口切片:可以存放任意类型(类似 JS 的混合数组)
    mixed := []interface{}{1, "two", true, 3.14}
    for _, v := range mixed {
        fmt.Println(v)
    }

    // map 值为空接口:类似 JS 的普通对象
    data := map[string]interface{}{
        "name": "杜宽",
        "age":  18,
        "tags": []string{"Go", "K8s"},
    }
    fmt.Println(data["name"])  // 杜宽
}
ts 复制代码
// TS 等效
function printAnything(v: any): void {
    console.log(`值: ${v}, 类型: ${typeof v}`)
}

// 混合数组
const mixed: any[] = [1, "two", true, 3.14]

// 任意值 map
const data: Record<string, any> = {
    name: "杜宽",
    age: 18,
    tags: ["Go", "K8s"],
}

注意 :空接口虽然灵活,但会丢失类型安全。Go 1.18+ 推荐用 anyinterface{} 的别名)提高可读性,但两者完全等价。

类型断言

从接口变量中取回具体类型的值,类似 TS 的类型断言 as

go 复制代码
package main

import "fmt"

func main() {
    var i interface{} = "hello"

    // 方式一:直接断言(不安全,类型不匹配会 panic)
    s := i.(string)
    fmt.Println(s)  // hello

    // 方式二:comma ok 模式(安全,推荐)
    s2, ok := i.(string)
    if ok {
        fmt.Println("断言成功:", s2)  // 断言成功: hello
    }

    // 断言失败不会 panic,ok 为 false
    n, ok := i.(int)
    fmt.Println(n, ok)  // 0 false

    // 接口变量断言为具体结构体
    var a Animal = Dog{Name: "旺财"}
    if dog, ok := a.(Dog); ok {
        fmt.Println("是一只狗:", dog.Name)  // 是一只狗: 旺财
    }
}
ts 复制代码
// TS 等效
let i: any = "hello"

// TS 类型断言(编译时,不做运行时检查)
const s = i as string
console.log(s)

// 运行时类型检查用 typeof / instanceof
if (typeof i === "string") {
    console.log("是字符串:", i)
}

Go vs TS 类型断言的关键区别

对比项 Go 类型断言 TS 类型断言
时机 运行时检查 编译时(不做运行时检查)
失败结果 panic(不安全模式)/ ok=false(安全模式) 编译通过,运行时可能出错
语法 v, ok := i.(T) v as T
安全模式 ✅ comma ok 模式 ❌ 需要手动 instanceof

类型 switch

当需要对多种类型分别处理时,用 type switch 比多个类型断言更简洁:

go 复制代码
package main

import "fmt"

// 处理不同类型的值(类似 JSON 解析后的字段处理)
func processValue(v interface{}) {
    switch val := v.(type) {
    case int:
        fmt.Printf("整数: %d, 翻倍后: %d\n", val, val*2)
    case string:
        fmt.Printf("字符串: %s, 长度: %d\n", val, len(val))
    case bool:
        fmt.Printf("布尔值: %v\n", val)
    case []int:
        fmt.Printf("整数切片: %v, 长度: %d\n", val, len(val))
    case nil:
        fmt.Println("空值")
    default:
        fmt.Printf("未知类型: %T\n", val)
    }
}

func main() {
    processValue(42)
    processValue("hello")
    processValue(true)
    processValue([]int{1, 2, 3})
    processValue(nil)
    processValue(3.14)
}
ts 复制代码
// TS 等效:用 typeof + instanceof 组合
function processValue(v: unknown): void {
    if (typeof v === "number") {
        console.log(`整数: ${v}, 翻倍后: ${v * 2}`)
    } else if (typeof v === "string") {
        console.log(`字符串: ${v}, 长度: ${v.length}`)
    } else if (typeof v === "boolean") {
        console.log(`布尔值: ${v}`)
    } else if (Array.isArray(v)) {
        console.log(`数组: ${v}, 长度: ${v.length}`)
    } else if (v === null || v === undefined) {
        console.log("空值")
    } else {
        console.log(`未知类型: ${typeof v}`)
    }
}

常用内置接口

Go 标准库定义了一批常用接口,理解它们有助于写出地道的 Go 代码:

error 接口
go 复制代码
// error 是 Go 内置接口,只有一个方法
type error interface {
    Error() string
}

// 自定义错误类型:实现 error 接口
type ValidationError struct {
    Field   string
    Message string
}

// 实现 Error() 方法,ValidationError 自动满足 error 接口
func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 验证失败: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Message: "年龄不能为负数"}
    }
    if age > 150 {
        return &ValidationError{Field: "age", Message: "年龄超出合理范围"}
    }
    return nil
}

func main() {
    err := validateAge(-1)
    if err != nil {
        fmt.Println(err.Error())  // 字段 age 验证失败: 年龄不能为负数

        // 类型断言取回具体错误类型
        if ve, ok := err.(*ValidationError); ok {
            fmt.Println("出错字段:", ve.Field)
        }
    }
}
ts 复制代码
// TS 等效:自定义 Error 子类
class ValidationError extends Error {
    constructor(public field: string, message: string) {
        super(`字段 ${field} 验证失败: ${message}`)
    }
}

function validateAge(age: number): Error | null {
    if (age < 0) return new ValidationError("age", "年龄不能为负数")
    if (age > 150) return new ValidationError("age", "年龄超出合理范围")
    return null
}
fmt.Stringer 接口

实现 Stringer 接口后,fmt.Println 会自动调用 String() 方法,类似 JS 的 toString()

go 复制代码
import "fmt"

type Point struct {
    X, Y int
}

// 实现 fmt.Stringer 接口
func (p Point) String() string {
    return fmt.Sprintf("Point(%d, %d)", p.X, p.Y)
}

func main() {
    p := Point{X: 3, Y: 4}
    fmt.Println(p)         // Point(3, 4)  --- 自动调用 String()
    fmt.Printf("%v\n", p)  // Point(3, 4)
    fmt.Printf("%s\n", p)  // Point(3, 4)
}
ts 复制代码
// TS 等效:重写 toString()
class Point {
    constructor(public x: number, public y: number) {}

    toString(): string {
        return `Point(${this.x}, ${this.y})`
    }
}

const p = new Point(3, 4)
console.log(String(p))  // Point(3, 4)
console.log(`${p}`)     // Point(3, 4)
io.Reader / io.Writer 接口

这是 Go 标准库中最重要的两个接口,几乎所有 I/O 操作都基于它们:

go 复制代码
// 标准库定义(了解即可)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}
go 复制代码
package main

import (
    "fmt"
    "strings"
    "io"
)

func main() {
    // strings.NewReader 返回一个实现了 io.Reader 的对象
    r := strings.NewReader("Hello, Go Interface!")

    // 任何接受 io.Reader 的函数都可以接收它
    buf := make([]byte, 8)
    for {
        n, err := r.Read(buf)
        if err == io.EOF {
            break
        }
        fmt.Printf("读取了 %d 字节: %s\n", n, buf[:n])
    }
}

为什么 io.Reader/Writer 重要:文件、网络连接、HTTP 请求体、压缩流等都实现了这两个接口,函数只需接受接口类型,就能处理所有这些数据源,无需关心具体实现。这是 Go 接口最强大的应用场景。

接口与 nil 的陷阱

Go 接口内部由两部分组成:(类型, 值)。只有两者都为 nil,接口才等于 nil:

go 复制代码
package main

import "fmt"

type MyError struct {
    msg string
}

func (e *MyError) Error() string { return e.msg }

// ❌ 常见陷阱:返回了非 nil 接口
func badFunction() error {
    var err *MyError = nil  // 具体类型指针为 nil
    return err              // 但接口的类型部分是 *MyError,不为 nil!
}

// ✅ 正确做法:直接返回 nil
func goodFunction() error {
    return nil  // 接口的类型和值都为 nil
}

func main() {
    err := badFunction()
    fmt.Println(err == nil)   // false!(反直觉)
    fmt.Println(err)          // <nil>(打印出来是 nil,但比较不等于 nil)

    err2 := goodFunction()
    fmt.Println(err2 == nil)  // true
}

规则 :函数返回 error 接口时,永远不要返回一个值为 nil 的具体类型指针 ,直接 return nil

接口设计原则

go 复制代码
// ✅ 小接口原则:接口方法越少越好,便于实现和组合
type Closer interface {
    Close() error
}

// ✅ 接口在使用方定义,不在实现方定义
// 函数需要什么行为,就在函数所在包定义接口
func processData(r io.Reader) error {
    // 只关心 Read 行为,不关心具体类型
    return nil
}

// ❌ 避免定义大而全的接口(难以实现,难以复用)
type DoEverything interface {
    Read() string
    Write(s string)
    Delete()
    Update(s string)
    List() []string
    // ...
}

Go 接口设计哲学 :接口越小越好。Go 标准库中大量接口只有 1-2 个方法(io.Readerio.Writererrorfmt.Stringer),这使得实现和组合都非常容易。

完整对比总结

对比项 Go 接口 TS 接口
实现方式 隐式(鸭子类型) 显式 implements
接口内容 只有方法 方法 + 属性
接口组合 嵌套接口 extends
空接口 interface{} / any any / unknown
类型检查 运行时类型断言 v.(T) 编译时 as T
多类型分支 type switch typeof + instanceof
接口变量 持有 (类型, 值) 二元组 编译时类型,运行时无接口概念
nil 陷阱 存在(类型非 nil 但值为 nil) 无此问题

易错点

go 复制代码
// ❌ 接口不能包含字段
type BadInterface interface {
    Name string  // 编译错误!接口只能有方法
}

// ✅ 接口只有方法
type GoodInterface interface {
    GetName() string
}

// ❌ 值类型实现了接口,但指针类型没有(反之则两者都有)
type Counter struct{ count int }

func (c *Counter) Increment() { c.count++ }  // 指针接收者

// var c Counter = Counter{}
// var i Incrementer = c  // ❌ 编译错误!Counter 没有实现,*Counter 才有

var c *Counter = &Counter{}
// var i Incrementer = c  // ✅ *Counter 实现了接口

// ✅ 规则:指针接收者方法只有指针类型满足接口;值接收者方法两者都满足

指针接收者 vs 值接收者与接口的关系

方法定义 值类型满足接口 指针类型满足接口
值接收者 func (t T) Method()
指针接收者 func (t *T) Method()

3. 并发编程

17.1 goroutine --- 轻量级并发单元

goroutine 是 Go 并发的核心,比操作系统线程轻量得多(初始栈约 2KB,线程约 1MB),由 Go 运行时调度,无需手动管理。

go 复制代码
// 普通函数调用(同步,阻塞)
sayHello("同步")

// go 关键字启动 goroutine(异步,立即返回)
go sayHello("异步")

// 匿名函数 goroutine
go func() {
    fmt.Println("匿名 goroutine")
}()

闭包陷阱:循环中启动 goroutine 时,闭包捕获的是变量引用,需要通过参数传值:

go 复制代码
// ❌ 错误:所有 goroutine 可能读到同一个 i 值
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()
}

// ✅ 正确:通过参数传入,每次循环创建独立副本
for i := 0; i < 3; i++ {
    i := i // shadowing:重新声明局部变量
    go func() { fmt.Println(i) }()
}

注意:main goroutine 退出后,所有子 goroutine 会被强制终止。

JS/TS 对比

概念 Go JS/TS
并发单元 goroutine(轻量,运行时调度) Promise / async-await(事件循环)
启动方式 go fn() fn() 返回 Promise,或 async fn()
并行执行 真正的多核并行 单线程,I/O 并发(Worker 除外)
等待完成 sync.WaitGroup Promise.all()

17.2 sync.WaitGroup --- 等待一组 goroutine 完成

WaitGroup 是协调多个 goroutine 完成的标准方式,比 time.Sleep 更可靠。

go 复制代码
var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
    wg.Add(1)           // 启动前 +1,不能放在 goroutine 内部
    go func(id int) {
        defer wg.Done() // 完成时 -1,defer 确保一定执行
        fmt.Println("任务", id, "完成")
    }(i)
}

wg.Wait() // 阻塞,直到计数器归零
fmt.Println("所有任务完成")

三个方法

方法 作用
wg.Add(n) 计数器 +n,在 go 语句之前调用
wg.Done() 计数器 -1,goroutine 完成时调用,等价于 Add(-1)
wg.Wait() 阻塞,直到计数器归零

常见错误Add 放在 goroutine 内部会导致竞态------Wait 可能在 Add 执行前就返回了。


17.3 channel --- goroutine 间通信

channel 是 goroutine 之间传递数据的管道,类型安全。Go 并发哲学:不要通过共享内存来通信,而要通过通信来共享内存

go 复制代码
// 创建 channel
unbuffered := make(chan int)    // 无缓冲,容量 0
buffered   := make(chan int, 5) // 有缓冲,容量 5

// 发送和接收
ch <- 42    // 发送
v := <-ch   // 接收
v, ok := <-ch // ok=false 表示 channel 已关闭且无数据

// 关闭(只有发送方关闭)
close(ch)

// range 接收,直到 channel 关闭
for v := range ch {
    fmt.Println(v)
}

无缓冲 vs 有缓冲

类型 发送行为 接收行为 适用场景
无缓冲 make(chan T) 阻塞,直到接收方就绪 阻塞,直到发送方发送 同步握手、信号传递
有缓冲 make(chan T, n) 缓冲区未满时不阻塞 缓冲区为空时阻塞 解耦生产者消费者

方向限定(函数参数中使用,增加类型安全):

go 复制代码
func producer(ch chan<- int) { ch <- 1 }  // 只写
func consumer(ch <-chan int) { v := <-ch } // 只读

注意事项

  • 向已关闭的 channel 发送数据会 panic
  • 关闭已关闭的 channel 会 panic
  • nil channel 的发送和接收都会永久阻塞

17.4 select --- 多路 channel 复用

select 让 goroutine 同时等待多个 channel 操作,类似 switch 但每个 case 是 channel 操作。多个 case 同时就绪时随机选择一个执行。

go 复制代码
// 基本用法
select {
case msg := <-ch1:
    fmt.Println("ch1:", msg)
case msg := <-ch2:
    fmt.Println("ch2:", msg)
}

// default:非阻塞操作(没有就绪的 case 时立即执行 default)
select {
case v := <-ch:
    fmt.Println("有数据:", v)
default:
    fmt.Println("暂无数据")
}

// 超时控制
select {
case result := <-ch:
    fmt.Println("结果:", result)
case <-time.After(100 * time.Millisecond):
    fmt.Println("超时!")
}

// for + select:持续监听,直到收到退出信号
for {
    select {
    case v := <-dataCh:
        process(v)
    case <-done:
        return // 退出
    }
}

JS/TS 对比

js 复制代码
// JS 中类似的模式:Promise.race(竞速,取最快的)
const result = await Promise.race([
    fetch('/api/data'),
    new Promise((_, reject) => setTimeout(() => reject('timeout'), 100))
])

Go 的 selectPromise.race 更灵活:可以发送也可以接收,可以有 default,可以在循环中持续使用。


17.5 sync.Mutex --- 互斥锁

当多个 goroutine 需要读写同一变量时,必须用锁保护,否则会产生数据竞争(data race)

go 复制代码
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()         // 加锁:其他 goroutine 调用 Lock 时阻塞
    defer c.mu.Unlock() // 解锁:defer 确保一定释放
    c.count++
}

sync.RWMutex:读多写少的场景,允许多个 goroutine 并发读,写时独占:

go 复制代码
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()         // 读锁:多个 goroutine 可同时持有
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()         // 写锁:独占,读写都阻塞
    defer c.mu.Unlock()
    c.data[key] = val
}

sync.Once:确保某段代码只执行一次(单例初始化):

go 复制代码
var once sync.Once
once.Do(func() {
    fmt.Println("只执行一次")
})

检测数据竞争 :运行时加 -race 标志:

bash 复制代码
go run -race main.go
go test -race ./...

17.6 context --- 控制 goroutine 生命周期

context 用于在 goroutine 树中传递取消信号、超时、截止时间和请求范围的值,是 Go 并发中控制 goroutine 生命周期的标准方式。

go 复制代码
// 四种创建方式
ctx := context.Background()                          // 根 context,程序入口
ctx, cancel := context.WithCancel(parent)            // 手动取消
ctx, cancel := context.WithTimeout(parent, 5*time.Second) // 超时自动取消
ctx, cancel := context.WithDeadline(parent, deadline)     // 指定截止时间
ctx  = context.WithValue(parent, key, value)         // 携带值(无 cancel)

// 使用完毕后调用 cancel 释放资源(即使提前完成也要调用)
defer cancel()

在函数中监听取消信号

go 复制代码
func doWork(ctx context.Context) error {
    select {
    case <-time.After(200 * time.Millisecond): // 正常完成
        return nil
    case <-ctx.Done(): // 收到取消/超时信号
        return ctx.Err() // context.Canceled 或 context.DeadlineExceeded
    }
}

context 传播:父 context 取消,所有子 context 也会被取消:

go 复制代码
parent, parentCancel := context.WithCancel(context.Background())
child, childCancel   := context.WithCancel(parent)
defer childCancel()

parentCancel() // 取消父,child 也会被取消

传递请求范围的值

go 复制代码
// key 使用自定义类型,避免与其他包冲突
type ctxKey string
ctx := context.WithValue(bg, ctxKey("requestID"), "req-123")

// 读取
if id, ok := ctx.Value(ctxKey("requestID")).(string); ok {
    fmt.Println("请求ID:", id)
}

JS/TS 对比

概念 Go JS/TS
取消操作 context.WithCancel + ctx.Done() AbortController + signal.abort()
超时 context.WithTimeout AbortSignal.timeout(ms)
传递请求数据 context.WithValue AsyncLocalStorage / 请求对象
传播取消 父 context 取消,子自动取消 需手动传递 signal

并发编程速查表

场景 工具 说明
启动异步任务 go fn() 轻量,运行时调度
等待多个任务完成 sync.WaitGroup Add/Done/Wait 三件套
goroutine 间传数据 channel 类型安全的管道
监听多个 channel select 随机选择就绪的 case
超时控制 select + time.After context.WithTimeout
保护共享变量 sync.Mutex Lock/Unlock 包裹临界区
读多写少的共享数据 sync.RWMutex RLock 允许并发读
只执行一次 sync.Once 单例初始化
取消/超时传播 context 贯穿整个调用链

Go 并发易踩坑点

  1. goroutine 泄漏 :goroutine 启动后没有退出机制,会一直占用资源;用 contextdone channel 控制退出
  2. 闭包捕获循环变量go func() { use(i) }() 中的 i 是引用,需要 i := i 或通过参数传入
  3. WaitGroup.Add 放在 goroutine 内 :竞态条件,Wait 可能在 Add 前返回
  4. 向已关闭的 channel 发送:panic,只有发送方应该关闭 channel
  5. 忘记调用 cancel()WithCancel/WithTimeout 返回的 cancel 必须调用,否则泄漏 context 资源
  6. 对 nil channel 操作:发送和接收都会永久阻塞,通常是 bug
  7. 无保护地并发写 map :Go 的 map 不是并发安全的,需要加锁或用 sync.Map


附录:与 JS/TS 语法速查表(进阶)

场景 Go JS/TS
接口定义 type I interface { Method() } interface I { method(): void }
接口实现 隐式,无需声明 显式 implements I
空接口 interface{} / any any / unknown
类型断言 v, ok := i.(T) v as T
类型 switch switch v := i.(type) { case T: } typeof / instanceof
接口组合 嵌套接口 type RW interface { R; W } interface RW extends R, W {}
结构体定义 type T struct { F int } interface T { f: number } / class T {}
结构体实例化 T{Field: val} { field: val } / new T()
结构体方法 func (t T) Method() {} class T { method() {} }
指针接收者 func (t *T) Modify() {} class T { modify() {} } (this 自动引用)
JSON 序列化 json.Marshal(v) JSON.stringify(v)
JSON 反序列化 json.Unmarshal(data, &v) JSON.parse(str)
goroutine go fn() Promise / async-await
等待并发完成 sync.WaitGroup Promise.all()
goroutine 通信 channel 无直接对应(EventEmitter / MessageChannel)
多路复用 select Promise.race()
互斥锁 sync.Mutex 无(单线程不需要)
取消/超时 context.WithCancel/Timeout AbortController

附录:Go 易踩坑点(前端视角)

  1. := 只能在函数内使用 ,包级别用 var
  2. 左花括号不能换行 ,必须 func main() {
  3. 未使用的变量/包 = 编译错误 ,用 _ 忽略
  4. 不同数字类型不能直接运算intint64 算不同类型
  5. 数组是值类型,赋值和传参会复制整个数组(不是引用)
  6. 切片截取是视图,修改子切片会影响原切片
  7. nil map 不能直接赋值 ,必须 make 初始化
  8. len() 对字符串返回字节数 ,中文占 3 字节,用 utf8.RuneCountInString 获取字符数
  9. Go 没有 try-catch ,错误是返回值,需要 if err != nil 检查
  10. ++ / -- 是语句不是表达式 ,不能 x := y++
  11. 结构体字段首字母小写 = 包外不可访问,对外暴露的字段必须大写
  12. 结构体是值类型 ,赋值会复制整个结构体;需要引用语义时用指针 *T
  13. 接口只能包含方法 ,不能有字段;实现是隐式的,无需 implements
  14. 指针接收者方法只有指针类型满足接口,值类型不满足;值接收者方法两者都满足
  15. 返回 error 接口时直接 return nil,不要返回值为 nil 的具体类型指针,否则接口不等于 nil
  16. goroutine 泄漏 :goroutine 启动后没有退出机制,会一直占用资源;用 contextdone channel 控制退出
  17. 闭包捕获循环变量go func() { use(i) }() 中的 i 是引用,需要 i := i 或通过参数传入
  18. WaitGroup.Add 放在 goroutine 内 :竞态条件,Wait 可能在 Add 前返回
  19. 向已关闭的 channel 发送:panic,只有发送方应该关闭 channel
  20. 忘记调用 cancel()WithCancel/WithTimeout 返回的 cancel 必须调用,否则泄漏 context 资源
  21. 对 nil channel 操作:发送和接收都会永久阻塞,通常是 bug
  22. 无保护地并发写 map :Go 的 map 不是并发安全的,需要加锁或用 sync.Map
相关推荐
鹏北海4 小时前
Go 包管理笔记 — 面向 JS/TS 前端开发者
go
百度Geek说5 小时前
告别死锁和陈旧语法、告别性能瓶颈:新手Gopher 秒变 Go 语言大神
人工智能·go
用户3983461612012 小时前
Go-Spring 实战第 14 课 —— Bean 注册函数:Provide、Module、Group 以及 Configuration
spring·go
锋行天下1 天前
一句mysql复杂查询搞崩一个壮汉
后端·mysql·go
用户398346161201 天前
Go-Spring 实战第 13 课 —— Bean 元信息:名称、生命周期、接口导出、条件和显式依赖
spring·go
猪猪拆迁队1 天前
用 ESP32-S3 和 TinyGo,先搭个 AI 语音助手的小底座
前端·后端·go
赫媒派2 天前
炸裂!Go 1.26 三连发:go fix 现代化、pkg.go.dev API 开放、源码级内联器
go
用户398346161202 天前
Go-Spring 实战第 11 课 —— 依赖注入的目标:单 Bean 注入和集合注入
spring·go
Coding君2 天前
每日一Go-68、基于 Kind 的 Istio 本地实战(完整可跑)
go