Go 语言进阶笔记 --- 面向 JS/TS 前端开发者
进阶部分:结构体、接口、并发编程
1. 结构体(Struct)
结构体是 Go 中最重要的数据组织方式,用于将多个字段组合成一个自定义类型。类似 JS/TS 中的 class 或 interface,但更轻量------没有继承,只有组合。
结构体定义
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+ 推荐用 any(interface{} 的别名)提高可读性,但两者完全等价。
类型断言
从接口变量中取回具体类型的值,类似 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.Reader、io.Writer、error、fmt.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 的 select 比 Promise.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 并发易踩坑点
- goroutine 泄漏 :goroutine 启动后没有退出机制,会一直占用资源;用
context或done channel控制退出 - 闭包捕获循环变量 :
go func() { use(i) }()中的i是引用,需要i := i或通过参数传入 - WaitGroup.Add 放在 goroutine 内 :竞态条件,
Wait可能在Add前返回 - 向已关闭的 channel 发送:panic,只有发送方应该关闭 channel
- 忘记调用 cancel() :
WithCancel/WithTimeout返回的 cancel 必须调用,否则泄漏 context 资源 - 对 nil channel 操作:发送和接收都会永久阻塞,通常是 bug
- 无保护地并发写 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 易踩坑点(前端视角)
:=只能在函数内使用 ,包级别用var- 左花括号不能换行 ,必须
func main() { - 未使用的变量/包 = 编译错误 ,用
_忽略 - 不同数字类型不能直接运算 ,
int和int64算不同类型 - 数组是值类型,赋值和传参会复制整个数组(不是引用)
- 切片截取是视图,修改子切片会影响原切片
- nil map 不能直接赋值 ,必须
make初始化 len()对字符串返回字节数 ,中文占 3 字节,用utf8.RuneCountInString获取字符数- Go 没有 try-catch ,错误是返回值,需要
if err != nil检查 ++/--是语句不是表达式 ,不能x := y++- 结构体字段首字母小写 = 包外不可访问,对外暴露的字段必须大写
- 结构体是值类型 ,赋值会复制整个结构体;需要引用语义时用指针
*T - 接口只能包含方法 ,不能有字段;实现是隐式的,无需
implements - 指针接收者方法只有指针类型满足接口,值类型不满足;值接收者方法两者都满足
- 返回 error 接口时直接
return nil,不要返回值为 nil 的具体类型指针,否则接口不等于 nil - goroutine 泄漏 :goroutine 启动后没有退出机制,会一直占用资源;用
context或done channel控制退出 - 闭包捕获循环变量 :
go func() { use(i) }()中的i是引用,需要i := i或通过参数传入 - WaitGroup.Add 放在 goroutine 内 :竞态条件,
Wait可能在Add前返回 - 向已关闭的 channel 发送:panic,只有发送方应该关闭 channel
- 忘记调用 cancel() :
WithCancel/WithTimeout返回的 cancel 必须调用,否则泄漏 context 资源 - 对 nil channel 操作:发送和接收都会永久阻塞,通常是 bug
- 无保护地并发写 map :Go 的 map 不是并发安全的,需要加锁或用
sync.Map