纯新手先看这一框
Go 适合做什么? 高并发服务端(微服务、API 网关、消息中间件)、云计算基础设施(Docker/Kubernetes 都是 Go 写的)、DevOps 工具、CLI 命令行程序。云原生时代它是事实标准语言。
为什么学它? 语法极简、编译飞快、天生支持并发(goroutine)、部署只要一个静态二进制文件(不用装运行时)。团队上手成本低,很适合写「稳、快、好部署」的后端。
怎么跑起来? ① 本页示例点 ▶ 运行 直接看结果;② 装 Go(go.dev 下 1.22+),把代码存成 main.go,命令行 go run main.go;③ go build 编译出可执行文件。
新手最常踩的坑? 没有「类继承」和「三元运算符」、错误处理要手写 if err != nil、for 是唯一循环、切片 append 可能共享底层数组。下面每节都会标红。
1. Hello World 与运行先跑通建立信心
Go 是编译型语言,但 go run 把「编译+运行」一步完成,体验类似脚本。
package main // 可执行程序必须叫 main
import "fmt" // 导入格式化输出包
func main() { // 程序入口
fmt.Println("Hello, World!")
fmt.Printf("2 + 3 = %d\n", 2+3)
}
package。main 包 + main() 函数才是可执行入口;其他包是「库」。未使用的导入和变量会编译报错(这是 Go 的强制整洁机制,新手一开始会烦,久了真香)。运行方式
go run main.go:编译到临时目录并运行,不留下文件。go build:生成同名可执行文件,直接./main运行。go fmt ./...:按官方风格自动格式化(别手写格式,交给工具)。
2. 变量与常量类型在后面,能推断就推断
var x int = 10 // 完整写法
var y = 20 // 类型推断
z := 30 // 短声明(函数内专用,最常用)
const PI = 3.14159 // 常量
// PI = 3 // 报错:常量不能改
// iota:常量计数器,自动生成递增序列
const (
A = iota // 0
B // 1
C // 2
)
:= 只能在函数内部用;包级别变量必须用 var。同作用域里 x := 1 后再 x := 2 会编译错——但 x, err := f() 只要有一个是新变量就允许(这是「短变量重声明」规则)。3. 基本类型零值 + 无隐式转换
| 类型 | 说明 | 零值 |
|---|---|---|
int/int64 | 整数(平台相关/固定64位) | 0 |
float64 | 浮点(默认用 64) | 0 |
string | UTF-8 不可变字节序列 | "" |
bool | true/false | false |
rune | int32 别名,一个 Unicode 码点 | 0 |
var a int = 5
var b int64 = 10
// c := a + b // 编译错!int 和 int64 不能混算
c := int64(a) + b // 必须显式转换
strconv.Itoa / strconv.FormatInt,别用 string(65)(那会当成 Unicode 码点变成 "A")。4. 流程控制只有 for,没有 while/三元
// if 条件不加括号
if x > 10 {
fmt.Println("大")
} else if x > 5 {
fmt.Println("中")
} else {
fmt.Println("小")
}
// for 是唯一的循环
for i := 0; i < 5; i++ { // 经典三段式
fmt.Println(i)
}
for x < 10 { // 当 while 用(条件前置)
x++
}
for { // 死循环,配 break 用
break
}
// switch 自动 break,不用写 case 后 break
switch day {
case "sat", "sun":
fmt.Println("周末")
default:
fmt.Println("工作日")
}
a?b:c 不存在,老老实实写 if/else。for-range 遍历时取到的 v 是元素的副本,改 v 不影响原切片;想改原值用下标 for i := range s { s[i] = ... }。5. 数组与切片切片才是日常,数组很少直接用
切片(slice)是 Go 最常用的序列类型,底层是个「指向数组的窗口」,含指针/长度/容量三部分。
s := []int{1, 2, 3} // 切片字面量
s = append(s, 4) // 追加 → [1 2 3 4]
fmt.Println(s[1:3]) // [2 3] 切片(共享底层数组)
// make 创建:长度3,容量5
buf := make([]int, 3, 5)
fmt.Println(len(buf), cap(buf)) // 3 5
append 不一定新建底层数组!当原切片容量足够时,append 会复用底层数组并覆盖后面的元素。如果两个切片共享底层数组,改一个会「串」到另一个。安全做法:用 copy 或 make + 重新切片隔离。避坑口诀:append 之后别再依赖旧切片的内容。nil(len=cap=0),但 nil 切片也能 append 和 range,不用像某些语言先判空。判断「是否为空」用 len(s) == 0,别用 s == nil(空切片可能非 nil)。6. map键值表,键类型有限制
m := make(map[string]int)
m["apple"] = 3
m["banana"] = 5
fmt.Println(m["apple"]) // 3
// 取键值 + 判断是否存在("comma ok" 惯用法)
v, ok := m["cherry"]
if ok {
fmt.Println(v)
} else {
fmt.Println("没有")
}
delete(m, "apple") // 删除键
nil,直接赋值会 panic!必须用 make 或字面量初始化。map 的键必须是可比较类型(string/int/struct 等),切片、map、func 不能做键。并发读写 map 会直接 panic——多 goroutine 访问要加锁或用 sync.Map。7. 函数多返回值 + defer + 变参
// 多返回值(Go 的标配)
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(7, 3)
// 命名返回值 + defer
func greet(name string) (msg string) {
defer fmt.Println("执行结束") // 函数 return 前最后执行
msg = "你好, " + name
return // 直接返回命名的 msg
}
// 变参
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
fmt.Println(sum(1, 2, 3))
defer 的执行顺序是后进先出(栈);而且 defer 后面的函数参数在声明时就求值,不是执行时。想延迟读变量要包一层闭包:defer func(){ fmt.Println(i) }()。8. 指针比 C 安全:无指针运算、有 GC
x := 10
p := &x // p 是 *int,指向 x
fmt.Println(*p) // 10 解引用
*p = 20
fmt.Println(x) // 20 x 被改了
// 函数内想改外部变量,传指针
func inc(n *int) { *n++ }
inc(&x)
p+1 偏移),也没有悬空指针野指针问题——有垃圾回收(GC)自动管理。返回局部变量地址是安全的:func f() *int { x:=1; return &x } 编译器会把 x 逃逸到堆上。这点和 C 完全不同,放心用。9. 结构体与方法组合优于继承
type User struct {
Name string
Age int
}
// 值接收者(不改原对象)
func (u User) SayHi() string {
return "我是 " + u.Name
}
// 指针接收者(可改原对象,大对象也该用指针避免拷贝)
func (u *User) Grow() {
u.Age++
}
u := User{"小明", 18}
fmt.Println(u.SayHi())
u.Grow()
fmt.Println(u.Age) // 19
// 组合("继承"的替代)
type Admin struct {
User // 匿名嵌入,Admin 直接拥有 User 的字段和方法
Level int
}
*T;否则用 T。但同一类型的方法集要一致,别混着来导致接口实现判断混乱。Go 没有继承,用「结构体嵌入」实现复用,这是刻意的设计——避免继承层级爆炸。10. 接口隐式实现,小接口为王
Go 接口是隐式实现的:类型只要实现了接口要求的方法,就自动算实现了该接口,不用 implements 声明。
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "汪汪" }
func announce(s Speaker) { // 接收任意 Speaker
fmt.Println(s.Speak())
}
announce(Dog{}) // Dog 自动满足 Speaker
// 空接口 interface{} 可装任何类型(Go1.18+ 用 any)
var anything any = 42
anything = "hello"
v, ok := i.(string),用 comma ok 避免断言失败 panic。interface{}(即 any)装了指针后判等是比地址不是比内容,别踩坑。标准库里 error 本身就是个接口。11. 并发:goroutine + channelGo 的杀手锏
goroutine 是轻量协程(几 KB 栈,可开百万个);channel 是 goroutine 之间传值的管道。Go 名言:「不要通过共享内存来通信,要通过通信来共享内存。」
package main
import (
"fmt"
"time"
)
func worker(id int, ch chan int) {
for n := range ch { // 从 channel 收,关了就退出
fmt.Printf("worker %d 收到 %d\n", id, n)
}
}
func main() {
ch := make(chan int, 3) // 缓冲大小 3
go worker(1, ch) // 启动一个 goroutine
for i := 1; i <= 3; i++ {
ch <- i // 往 channel 发
}
close(ch)
time.Sleep(time.Millisecond) // 等 worker 打印完
}
sync.WaitGroup 或 channel 同步,否则 goroutine 可能没跑完程序就结束了。context.Context 来取消 goroutine 和设超时,而不是依赖 time.Sleep 这种凑合办法。12. 错误处理error 是值,不是异常
import (
"errors"
"fmt"
)
func mayFail(n int) (int, error) {
if n < 0 {
return 0, errors.New("n 不能为负")
}
return n * 2, nil
}
func main() {
r, err := mayFail(-1)
if err != nil { // Go 的标配:显式检查
fmt.Println("出错:", err)
return
}
fmt.Println(r)
}
if err != nil 检查——这是 Go 最被吐槽也最被喜爱的地方。把 err 静默忽略(_ = f())是常见 bug 来源。Go 1.13+ 用 errors.Is/errors.As 判断包装过的错误,用 fmt.Errorf("...: %w", err) 包装上层错误。panic 是给「真不可能发生」的致命错误用的(类似异常),正常业务流程别用。用 defer func(){ if r := recover(); r != nil { ... } }() 在 goroutine 顶层兜住 panic,防止一个 goroutine 崩了拖垮整个进程(尤其是 HTTP handler 里)。13. 包管理与可见性大写才导出
// go.mod 是项目清单
// module example.com/hello
// go 1.22
package mathutil
func Add(a, b int) int { return a + b } // 大写:对外可见(导出)
func secret() int { return 42 } // 小写:包内私有
go mod init 名字 初始化模块,go get 拉依赖,全部记录在 go.mod / go.sum。14. 标准库与 context开箱即用的云原生工具链
Go 标准库异常强大,很多场景不用第三方包:
| 包 | 用途 |
|---|---|
net/http | 写 Web 服务/客户端(见实战节) |
encoding/json | JSON 序列化(结构体加 json tag) |
strings/strconv | 字符串处理/类型转换 |
time | 时间、时区、定时器 |
context | 取消、超时、传值(贯穿并发) |
sync | WaitGroup/Mutex/Once/Map |
context.Context 的取消信号要沿着调用链一直传下去,别传 nil(用 context.Background() 作根)。JSON 字段想导出,结构体字段必须大写并加 `json:"name"` tag,否则序列化时丢字段。15. 测试与基准内置 testing,无需框架
// add_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
if Add(2, 3) != 5 {
t.Fatal("Add 算错了")
}
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ { // b.N 由测试框架决定跑多少次
Add(2, 3)
}
}
xxx_test.go,函数 TestXxx/BenchmarkXxx。跑:go test ./...、基准 go test -bench=. -benchmem。配合 -race 标志能检测数据竞争(并发 bug 神器)。16. 实战:并发 Web 服务拼起前面所有知识点
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
select {
case <-time.After(100 * time.Millisecond):
fmt.Fprintln(w, "pong")
case <-ctx.Done():
http.Error(w, "超时", http.StatusGatewayTimeout)
}
})
fmt.Println("监听 :8080")
http.ListenAndServe(":8080", mux)
}
context 做请求级超时;编译后是单个二进制,./server 直接跑,没有任何运行时依赖——这正是 Go 在云原生里横着走的原因。🧪 动手实验室代码已写好,点 ▶ 直接看结果(手机/电脑通用)
下面每个程序都已预填好。点 ▶ 运行 看输出;想改就在框里改再运行;右上角可切语言、↺ 重置。
① 基础:打印与循环
② 切片与 append
③ 函数:多返回值
④ 结构体与方法
⑤ 并发:goroutine + channel
💡 连老师都不会教你的干货(工业级技巧)学校不教,公司天天用
- 无隐式转换:
int和int64不能混算,必须显式转;数字转字符串用strconv,别用string(65)(那是码点 'A')。 - 切片 append 的共享坑:容量够时 append 复用底层数组,可能「串」改其他切片。需要隔离就用
copy或重新make。 - range 取的是副本:遍历时
v是元素拷贝,改 v 不动原切片;改原值用下标for i := range s。 - defer 是栈 + 即时求值:多个 defer 后进先出;参数在 defer 声明时就定下,想延迟读变量要包闭包。
- 零值可用:未初始化的变量自动是零值(0/""/false/nil),
nil切片/map 也能 range 和 append;但 map 必须先make才能写。 - goroutine 泄漏:往没人收的 channel 发数据会永久阻塞;用
context传递取消信号,主函数退出不等 goroutine。 - 通信优于共享内存:多 goroutine 协作优先用 channel,而非加锁共享变量;加锁也优先
sync而非自己造轮子。 - 接口隐式实现:不用
implements,只要方法集匹配就自动满足;优先定义小而专的接口(io.Reader这种),别搞大而全的接口。 - error 是值:异常流程用多返回值
error+if err != nil,别滥用panic;用%w包装、errors.Is/As判断。 - 调度器 GMP:Go 用 G(goroutine)/M(线程)/P(处理器)的 M:N 调度,默认并行度 = CPU 核数(
GOMAXPROCS),理解它能解释很多性能现象。 - map 并发会 panic:多 goroutine 读写同一 map 直接崩,用
sync.Map或sync.RWMutex保护。 - 方法接收者选择:要改对象/结构体大 → 指针接收者;只读且小 → 值接收者;同一类型别混用。
- 可见性靠大小写:首字母大写导出、小写私有,没有 private 关键字;包名要短而准。
- 工具链即规范:
gofmt统一格式、go vet抓可疑代码、-race查竞争、golangci-lint做团队 lint,CI 里全跑。 - string 拼接用 strings.Builder:循环里拼大量字符串用
strings.Builder或bytes.Buffer,比+=少分配很多次。 - context 别传 nil:根用
context.Background(),沿调用链传递取消与超时;请求处理里用r.Context()。 - 部署零依赖:
CGO_ENABLED=0 go build出纯静态二进制,丢到任何同架构 Linux 直接跑,容器镜像可以小到几 MB(scratch 基础镜像)。
📝 自测题点「查看答案」核对,先自己想
1. 以下代码输出什么?
s := []int{1, 2, 3}
s2 := append(s, 4)
s[0] = 99
fmt.Println(s2[0])
查看答案
可能输出 99 也可能不是——取决于 append 时是否扩容。容量 3 的切片 append 后容量变 6(翻倍),此时会分配新底层数组,s2 不共享,输出 1;但若原容量足够复用,就会输出 99。结论:append 后不要再依赖旧切片内容。
2. Go 里如何表示「三目运算符」a?b:c?
查看答案
没有。Go 刻意不提供三元运算符,必须写完整的 if/else。这是 Rob Pike 等设计者的取舍:让代码更直白、避免嵌套地狱。
3. 未初始化的 map 直接赋值会怎样?
查看答案
panic(运行时崩溃)。map 零值是 nil,必须先用 make(map[K]V) 或字面量初始化才能写。读取 nil map 不会崩(返回零值),但写入会崩。
4. 两个 goroutine 并发读写同一个 map,会发生什么?
查看答案
Go 运行时检测到并发读写 map 会直接 panic 并崩溃(不是数据错乱,是主动 abort)。正确做法:加 sync.RWMutex 或用 sync.Map。
5. 为什么 Go 说「不要通过共享内存来通信」?
查看答案
因为加锁共享内存容易出竞争 bug 且难调试。Go 鼓励用 channel 在 goroutine 之间传值,把「共享」变成「传递所有权」,逻辑更清晰、更难写错。这是 CSP(Communicating Sequential Processes)模型。