cv.cm

编码 · 编程语言

Go 语言入门教程(零基础)

阅读约 9 分钟

简短回答

Go(常被称为 Golang)是 Google 推出的简洁、编译型、静态类型语言,为快速构建、轻松并发和可靠的服务器而设计。安装 Go,编写一个 main 包,用 go run hello.go 运行。核心概念包括:精简的一组类型、切片与 map、带方法的结构体、隐式接口、用于并发的 goroutine 与 channel,以及作为普通值返回的错误。

打开JSON 格式化

Go 是什么,用来做什么

Go 由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 设计,2009 年以开源形式发布。它编译为单个静态二进制文件,启动速度快,带有垃圾回收器和丰富的网络标准库。它被用于云基础设施和命令行工具:Docker、Kubernetes、Terraform 和 Prometheus 都是用 Go 编写的,许多公司也用它开发 Web API 和微服务。这门语言刻意保持小巧,因此不同团队写出的代码风格相似。

安装 Go 并运行 Hello, world

从 go.dev/dl 下载 Go,并用 go version 检查。新建一个文件夹,初始化模块,然后添加 main.go:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}
go mod init example.com/hello
go run .
# Hello, world
go build     # produces a standalone binary named hello

程序从 package main 中的 func main 开始执行。运行 gofmt(或 go fmt)来格式化代码;所有 Go 代码都使用同一种风格,真实的源文件用制表符缩进。

变量与基本类型

用 var name type = value 声明变量,或在函数内部使用会自动推断类型的简短形式 name := value。未赋值的变量会得到零值:0、""、false 或 nil。

类型示例零值
int, int64n := 420
float64x := 3.140
boolok := truefalse
strings := "hi"""
runer := 'é'0(一个 Unicode 码点)
byteb := byte(65)0(uint8 的别名)
package main

import (
    "fmt"
    "strconv"
)

func main() {
    name := "Ada"
    count := 7
    avg := float64(count) / 2 // explicit conversion is required
    fmt.Printf("%s has %d items, avg %.1f\n", name, count, avg) // Ada has 7 items, avg 3.5
    n, err := strconv.Atoi("12")
    fmt.Println(n+1, err) // 13 <nil>
}

Go 从不在数值类型之间隐式转换,编译器还会拒绝未使用的变量和未使用的 import。

流程控制与函数

条件两边没有圆括号,花括号则是必需的。for 是唯一的循环关键字:经典循环、类似 while 的循环和 range 循环都用它。函数可以返回多个值。

package main

import "fmt"

func divmod(a, b int) (int, int) {
    return a / b, a % b
}

func main() {
    score := 72
    if score >= 90 {
        fmt.Println("A")
    } else if score >= 70 {
        fmt.Println("B") // B
    } else {
        fmt.Println("C")
    }

    for i := 0; i < 3; i++ {
        fmt.Print(i, " ") // 0 1 2
    }
    fmt.Println()

    n := 10
    for n > 1 { // while-style loop
        n /= 2
    }

    q, r := divmod(17, 5)
    fmt.Println(n, q, r) // 1 3 2

    switch day := "SAT"; day {
    case "SAT", "SUN":
        fmt.Println("weekend") // no break needed
    default:
        fmt.Println("weekday")
    }
}

切片、map 与结构体

切片是数组的可增长视图,是日常使用的列表类型。map 是哈希表。结构体把具名字段组织在一起,方法则是带接收者的函数。

package main

import (
    "fmt"
    "sort"
)

type Point struct {
    X, Y int
}

func (p Point) Add(o Point) Point { return Point{p.X + o.X, p.Y + o.Y} }

func (p *Point) Scale(k int) { p.X *= k; p.Y *= k } // pointer receiver modifies p

func main() {
    nums := []int{3, 1, 2}
    nums = append(nums, 5)
    sort.Ints(nums)
    fmt.Println(nums, len(nums), nums[1:3]) // [1 2 3 5] 4 [2 3]

    ages := map[string]int{"ann": 31, "bob": 25}
    ages["cy"] = 40
    age, ok := ages["zed"]
    fmt.Println(age, ok, len(ages)) // 0 false 3

    for i, n := range nums {
        fmt.Print(i, ":", n, " ") // 0:1 1:2 2:3 3:5
    }
    fmt.Println()

    p := Point{1, 2}.Add(Point{3, 4})
    p.Scale(2)
    fmt.Printf("%+v\n", p) // {X:8 Y:12}
}

遍历 map 的顺序是刻意随机的;需要稳定顺序时请对键排序。以大写字母开头的名称(Point、Add)会从包中导出。

接口

接口列出方法签名。一个类型只要拥有这些方法就满足该接口,不存在 implements 关键字。

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct{ R float64 }
type Rect struct{ W, H float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (r Rect) Area() float64   { return r.W * r.H }

func main() {
    shapes := []Shape{Circle{1}, Rect{2, 3}}
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    fmt.Printf("%.2f\n", total) // 9.14
}

Go 自 1.18 起也支持泛型,例如 func Max[T int | float64](a, b T) T,你大多是通过 slices 和 maps 包等库函数来使用它。

标志性特性:goroutine 与 channel

goroutine 是用 go 关键字启动的轻量级线程;一个程序可以同时运行成千上万个。goroutine 之间通过 channel 通信,它能在 goroutine 之间安全地传递值。sync.WaitGroup 用来等待一组 goroutine 结束。

package main

import (
    "fmt"
    "sync"
)

func square(n int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    results <- n * n
}

func main() {
    results := make(chan int, 5)
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go square(i, results, &wg)
    }
    wg.Wait()
    close(results)

    sum := 0
    for r := range results {
        sum += r
    }
    fmt.Println(sum) // 55
}

defer 会安排一次调用在外层函数返回时执行,Go 代码就是靠它来关闭文件和解锁互斥锁的。开发时可用 go run -race . 检测数据竞争。

错误处理

对于普通的失败,Go 没有异常。可能失败的函数把 error 作为最后一个返回值,调用者立即检查它。用 %w 包装错误可以添加上下文,同时仍能用 errors.Is 检查原始错误。

package main

import (
    "errors"
    "fmt"
    "os"
)

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }
    return data, nil
}

func main() {
    _, err := readConfig("missing.json")
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("no config file:", err)
    }
}

panic 和 recover 是存在的,但仅用于程序错误和真正意外的状态,不用于常规的错误处理。

一个完整的小程序:迷你 Web 服务器

只用标准库,这个程序就能通过 HTTP 提供 JSON 服务。用 go run . 运行,然后打开 http://localhost:8080/hello?name=Ann。

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type Greeting struct {
    Message string    `json:"message"`
    Time    time.Time `json:"time"`
}

func hello(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "world"
    }
    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(Greeting{"Hello, " + name, time.Now()})
    if err != nil {
        log.Println("encode:", err)
    }
}

func main() {
    http.HandleFunc("/hello", hello)
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

每个字段类型后面的文本是结构体标签(struct tag),用来设置 JSON 键名。每个请求都会自动在各自的 goroutine 中处理。

新手常见错误

  • 用 _ 忽略错误。除非你确定无关紧要,否则要检查每一个返回的 error。
  • 以为 append 会原地修改原切片;务必使用它的返回值,如 s = append(s, x)。
  • 启动了 goroutine,却让 main 在它们结束之前就返回。请用 sync.WaitGroup 或 channel 等待。
  • 方法需要修改结构体时却使用了值接收者;请使用指针接收者,例如 func (p *Point)。

接下来学什么

先做交互式的 Tour of Go,再阅读 Effective Go 和 pkg.go.dev 上的标准库文档。学习用 go test 做单元测试和基准测试,用 context 包处理超时和取消,用模块管理依赖。

常见问题

该叫 Go 还是 Golang?

官方名称是 Go。“Golang”来自早期的网站域名 golang.org,如今仍被用作搜索词。

Go 是面向对象的吗?

部分是。Go 有方法和接口,但没有类和继承;你通过嵌入结构体和组合小接口来构建类型。

Go 比 Python 快吗?

对大多数 CPU 密集型工作来说是的,因为 Go 编译为本机机器码并且有静态类型。当繁重的工作发生在用 C 编写的库内部时,Python 也可以很快。

goroutine 和线程有什么区别?

goroutine 由 Go 运行时管理,起始栈很小,会按需增长。运行时把大量 goroutine 调度到少量操作系统线程上,所以创建 goroutine 的开销要低得多。

全部编码教程