Fadey's Blog

arrow_back 返回首页
Go语言封面

Go 语言入门:从语法基础到并发编程

Go 的设计哲学一句话可以概括:少即是多。没有继承、没有泛型滥用(直到 1.18 才谨慎引入)、没有异常机制,但用 goroutine 和 channel 把并发做成了语言级别的第一公民。这篇文章面向有编程基础的读者,用最短的路径带你从语法走到并发。

安装与第一个程序

# 下载安装:https://go.dev/dl/ (Windows/macOS/Linux 都有安装包)
go version        # 确认安装成功,如 go version go1.23.0 windows/amd64

mkdir hello && cd hello
go mod init example.com/hello   # 初始化模块
package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}
go run .        # 直接运行
go build -o hello .   # 编译成二进制(无运行时依赖,直接丢服务器就能跑)

「编译出静态单文件二进制」是 Go 在部署上最大的卖点之一——不需要目标机器装任何运行时。

包与模块

  • go.mod:模块定义文件,记录模块名和依赖版本;
  • 大写开头 = 导出:语言层面用大小写控制可见性,不需要 public/private 关键字;
  • go mod tidy:整理依赖(增删 + 校验),提交代码前必跑。
// 目录即包:util/strings.go 的包名是 util
package util

// Exported:大写开头,包外可访问
func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

// unexported:小写开头,仅包内可见
func normalize(s string) string { return s }

变量、类型与「零值」哲学

var a int          // 零值 0
var s string       // 零值 ""(不是 nil,Go 里字符串永远有值)
var p *int         // 零值 nil

// 短变量声明(函数内使用,最常用)
count := 0
name := "Fadey"

// 多重赋值与交换
x, y := 1, 2
x, y = y, x

// 常量与 iota 枚举
type Status int

const (
    StatusTodo Status = iota   // 0
    StatusDoing                 // 1
    StatusDone                  // 2
)

零值可用是 Go 的一个核心设计:var buf bytes.Buffer 拿来就能用,不需要构造函数。写类型时尽量让自己的结构体也满足这个性质,调用方会感谢你。

切片、map 与遍历

// 切片:长度可变的数组视图(底层 = 指针 + 长度 + 容量)
nums := []int{1, 2, 3}
nums = append(nums, 4)          // 容量不够时自动扩容
sub := nums[1:3]                // [2 3],共享底层数组要小心

// map:键值对
scores := map[string]int{"alice": 90}
scores["bob"] = 85
v, ok := scores["carol"]        // 逗号-ok 惯用法:判存在
if !ok {
    fmt.Println("没找到 carol")   // v 是 int 零值 0
}

for i, v := range nums {        // 需要索引用 i,不需要用 _
    fmt.Println(i, v)
}
for k := range scores {         // map 遍历顺序是随机的(故意的)
    fmt.Println(k)
}

注意 range 的经典坑:循环变量在 Go 1.22 之前是共享的,闭包里捕获会拿到最后一个值;新版本每轮迭代都是新变量,但这个坑在旧代码里仍然遍地都是。

函数、方法与接口

// 多返回值:错误作为第二个返回值是铁律
func div(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("除数不能为 0")
    }
    return a / b, nil
}

// 值接收者 vs 指针接收者:需要修改状态用指针
type Counter struct{ n int }

func (c *Counter) Inc()      { c.n++ }
func (c Counter) Value() int { return c.n }

// 接口:隐式实现——不需要 implements,方法集对上了就成立
type Stringer interface {
    String() string
}

type User struct{ Name string }

func (u User) String() string { return "User(" + u.Name + ")" }

func printAll(items []Stringer) {
    for _, it := range items {
        fmt.Println(it.String())
    }
}

func main() {
    c := &Counter{}
    c.Inc()
    fmt.Println(c.Value())      // 1

    printAll([]Stringer{User{Name: "Fadey"}})  // 无需显式声明 implements
}

隐式实现让接口可以「用完才定义」:你先写库,用户后来按需定义小接口(io.Reader 只有一个方法),组合自由度和测试易用性都极高。

错误处理:没有异常,只有 error

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")   // 哨兵错误:供 errors.Is 判断

func findUser(id int) (*User, error) {
    if id <= 0 {
        // %w:包装错误并保留错误链,调用方仍能用 errors.Is/As 判断
        return nil, fmt.Errorf("findUser(id=%d): %w", id, ErrNotFound)
    }
    return &User{Name: "Fadey"}, nil
}

func main() {
    _, err := findUser(-1)
    if err != nil {
        if errors.Is(err, ErrNotFound) {     // 沿错误链匹配
            fmt.Println("按业务处理:返回空页面")
        } else {
            fmt.Println("未知错误,上报:", err)
        }
    }
}

三件套记住就够:errors.New 造哨兵错误、fmt.Errorf("%w") 包装、errors.Is / errors.As 判断。不要用字符串比较错误,那是最容易在重构中碎掉的写法。

并发:goroutine 与 channel

go 关键字启动一个 goroutine(几 KB 栈、由运行时调度),channel 是它们之间通信的管道。格言:不要通过共享内存来通信,而要通过通信来共享内存。

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for j := range jobs {                 // 通道关闭后循环自动结束
        time.Sleep(100 * time.Millisecond) // 模拟耗时任务
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 10)      // 带缓冲:发送方不被立刻阻塞
    results := make(chan int, 10)
    var wg sync.WaitGroup

    for w := 1; w <= 3; w++ {       // 3 个 worker 并发消费
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    for i := 1; i <= 6; i++ {
        jobs <- i
    }
    close(jobs)                      // 关闭后 range 结束、worker 退出

    wg.Wait()                        // 等全部 worker 完成
    close(results)

    for r := range results {
        fmt.Println("结果:", r)
    }
}

select 与 context:并发的控制面

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

// select:等待多个通道中的任意一个就绪(哪个先来走哪个)
func or(chans ...chan string) <-chan string {
    out := make(chan string, 1)
    go func() {
        for _, c := range chans {
            select {
            case v := <-c:
                out <- v
            }
        }
    }()
    return out
}

// context:取消信号沿调用链传播,超时/关停的标准做法
func fetchWithTimeout(ctx context.Context, url string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()   // 必须调用,否则泄漏定时器

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return "", err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", fmt.Errorf("请求 %s: %w", url, err)   // ctx 超时会体现在 err 里
    }
    defer resp.Body.Close()

    return resp.Status, nil
}

工程上的铁律:每个耗时函数第一个参数是 ctx context.Context,取消和超时才能贯穿整条调用链;goroutine 之间用 defer + WaitGroup 管理生命周期,不要「发射后不管」。

值得先学的标准库

  • net/http:标准库自带生产级 HTTP 服务器,80 行能写一个 JSON API;
  • encoding/json:结构体标签(`json:"name"`)做序列化,零依赖;
  • log/slog(1.21+):结构化日志,直接输出 JSON 给日志系统;
  • testing:go test ./... 内置测试与基准,不需要第三方框架起步;
  • database/sql + 驱动:连接池、预处理、事务的标准接口。
Go 用「显式」换掉了很多语言的「聪明」:错误显式返回、并发显式通信、依赖显式声明。第一周嫌啰嗦,第三个月会感谢它。

总结

这次入门路线覆盖了 Go 的骨架:模块与包 → 变量类型零值 → 切片 map → 方法接口 → error 三件套 → goroutine/channel/select/context。建议的下一步:用 net/http + database/sql 写一个带增删改查的小服务,再用 go test 补上测试——写完这一个项目,Go 的思维方式就基本成型了。