第一章:你好,Go!不只是打印一行字
很多刚接触Go的朋友,第一件事就是打开终端,敲下:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
然后满心欢喜地运行,看到输出后觉得“这语言挺简单”。但我想告诉你的是——这行简单的代码背后,藏着Go设计的哲学。
为什么选择Go?
在回答这个问题之前,先看看Go诞生的背景。2007年,Google的Robert Griesemer、Rob Pike和Ken Thompson开始思考一个根本问题:为什么软件工程如此艰难?
他们看到C++的代码库越来越大,编译时间越来越长,并发模型越来越复杂。于是Go诞生了,它的核心设计目标是:
- 编译速度快(接近C)
- 运行效率高(接近C++)
- 并发模型简洁(比线程轻量得多)
- 语法简洁易学(比C++简单,比Python有约束)
第一个真正重要的概念:包(package)
Go程序的基础单位不是“类”或“对象”,而是包。每个.go文件都必须属于某个包,而main包是程序的入口。
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("当前时间:", time.Now())
}
注意看import的写法——多个包用括号括起来,这是Go社区推荐的标准格式。用go fmt格式化代码,会让你的import自动整理成这种风格。
第二章:类型系统——简洁但严谨
Go的类型系统看起来简单,但每一个设计都有其深意。
基础类型速览
// 整数类型
var a int = 42 // 默认int,平台相关(32或64位)
var b int8 = 127 // 8位整数,范围-128到127
var c int16 = 32767 // 16位整数
var d int32 = 9999 // 32位整数,也常用作rune(Unicode码点)
var e int64 = 9000000000000000000 // 64位整数,处理大数或时间戳
// 无符号整数
var f uint = 42
var g uint8 = 255
// 浮点数
var h float32 = 3.14
var i float64 = 3.141592653589793 // Go默认浮点数是float64
// 布尔值
var j bool = true
// 字符串(不可变,UTF-8编码)
var k string = "Hello, Go"
类型转换——Go不允许隐式转换
这是很多从Python或JavaScript转过来的开发者最容易踩的坑:
package main
import "fmt"
func main() {
var x int = 42
var y float64 = float64(x) // 必须显式转换!
fmt.Printf("x=%d, y=%f\n", x, y)
// 下面这行会编译错误:
// var z int = y // 错误:cannot use y (type float64) as type int in assignment
// 正确做法
var z int = int(y)
fmt.Println(z)
}
Go的设计哲学是:显式优于隐式。编译器拒绝帮你做可能丢失精度的转换,这样能避免很多运行时bug。
零值——Go的温柔陷阱
在Go中,每个变量都有零值,即使你没显式赋值:
package main
import "fmt"
func main() {
var i int // 零值是 0
var s string // 零值是 ""
var b bool // 零值是 false
var p *int // 零值是 nil(空指针)
var m map[string]int // 零值是 nil(不是空map!)
fmt.Printf("int: %d, string: '%s', bool: %v, ptr: %v, map: %v\n",
i, s, b, p, m)
// 关键点:map、slice、chan、func、interface 的零值是 nil
// 使用 nil map 会 panic!
m["key"] = 1 // panic: assignment to entry in nil map
}
这就是Go新手常遇到的第一个panic:“attempt to index nil slice” 或 “assignment to entry in nil map”。
解决方案:
// 错误做法
var m map[string]int
m["key"] = 1 // panic!
// 正确做法1:用 make 初始化
m := make(map[string]int)
m["key"] = 1 // 安全
// 正确做法2:使用字面量
m := map[string]int{
"key": 1,
}
第三章:控制流——没有switch的默认break
Go的控制流语句简洁而强大,有一个关键特性:switch不会自动fall-through。
if/else
package main
import "fmt"
func checkAge(age int) string {
if age < 0 {
return "无效年龄"
} else if age < 18 {
return "未成年人"
} else if age < 60 {
return "成年人"
} else {
return "老年人"
}
}
func main() {
fmt.Println(checkAge(25)) // 输出:成年人
}
Go的if有一个独特特性:可以在条件前初始化变量:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
if num := rand.Intn(10); num > 5 {
fmt.Printf("%d 大于 5\n", num)
} else {
fmt.Printf("%d 小于等于 5\n", num)
}
// num 在这里已经不可访问,作用域被限制在if块内
// fmt.Println(num) // 编译错误:undefined: num
}
switch——简洁的分支处理
package main
import "fmt"
func getWeekdayName(day int) string {
switch day {
case 1:
return "星期一"
case 2:
return "星期二"
case 3:
return "星期三"
case 4:
return "星期四"
case 5:
return "星期五"
case 6:
return "星期六"
case 7:
return "星期日"
default:
return "无效日期"
}
}
func main() {
fmt.Println(getWeekdayName(3)) // 输出:星期三
}
Go的switch可以不带表达式,当作if-else使用:
package main
import "fmt"
func describeNumber(n int) {
switch {
case n < 0:
fmt.Println("负数")
case n == 0:
fmt.Println("零")
case n > 0 && n <= 10:
fmt.Println("小正数")
default:
fmt.Println("大正数")
}
}
func main() {
describeNumber(-5) // 输出:负数
describeNumber(5) // 输出:小正数
describeNumber(100)// 输出:大正数
}
关键区别:Go的switch不会自动fall-through,除非你显式使用fallthrough关键字:
package main
import "fmt"
func main() {
day := 2
switch day {
case 1:
fmt.Println("周一")
fallthrough // 显式fallthrough,继续执行case 2
case 2:
fmt.Println("周二") // 无论day是1还是2,都会执行这行
case 3:
fmt.Println("周三")
default:
fmt.Println("其他")
}
}
第四章:函数——多返回值与命名返回值
Go的函数支持多返回值,这是它区别于很多语言的重要特性。
基本函数定义
package main
import "fmt"
// 简单函数
func add(a, b int) int {
return a + b
}
// 多返回值
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}
func main() {
result := add(3, 4)
fmt.Println(result) // 输出:7
quotient, err := divide(10, 3)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("商: %.2f\n", quotient) // 输出:商: 3.33
}
命名返回值——让函数更可读
package main
import "fmt"
// 命名返回值:函数签名已经说明了返回值的含义
func calculate(a, b int) (sum, product int) {
sum = a + b
product = a * b
return // 直接返回,不需要写 return sum, product
}
func main() {
s, p := calculate(5, 3)
fmt.Printf("和: %d, 积: %d\n", s, p) // 输出:和: 8, 积: 15
}
变长参数
package main
import "fmt"
// 变长参数:接受任意数量的int
func sum(values ...int) int {
total := 0
for _, v := range values {
total += v
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 输出:6
fmt.Println(sum(1, 2, 3, 4, 5)) // 输出:15
}
闭包——函数的函数
package main
import "fmt"
// 返回一个函数
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println(double(5)) // 输出:10
fmt.Println(triple(5)) // 输出:15
}
重要陷阱:闭包捕获的是变量的引用,而不是值:
package main
import "fmt"
func main() {
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
// 错误:所有闭包都捕获同一个变量 i
funcs[i] = func() {
fmt.Printf("i = %d\n", i) // 都会输出 3!
}
}
for _, f := range funcs {
f()
}
// 正确做法:创建局部变量
funcs2 := make([]func(), 3)
for i := 0; i < 3; i++ {
j := i // 创建局部副本
funcs2[i] = func() {
fmt.Printf("i = %d\n", j) // 正确输出 0, 1, 2
}
}
for _, f := range funcs2 {
f()
}
}
第五章:结构体与方法——Go的OOP方式
Go没有类,但有结构体和方法。这是Go面向对象编程的核心。
基本结构体
package main
import (
"fmt"
"math"
)
// 定义结构体
type Circle struct {
X, Y, Radius float64
}
type Rectangle struct {
X, Y, Width, Height float64
}
// 方法:定义在结构体上
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 方法集:所有方法都叫 Area/Perimeter,可以通过接口统一调用
func printShapeInfo(s shape) {
fmt.Printf("面积: %.2f, 周长: %.2f\n", s.Area(), s.Perimeter())
}
// 接口定义
type shape interface {
Area() float64
Perimeter() float64
}
func main() {
c := Circle{X: 0, Y: 0, Radius: 5}
r := Rectangle{X: 0, Y: 0, Width: 4, Height: 6}
printShapeInfo(c) // 面积: 78.54, 周长: 31.42
printShapeInfo(r) // 面积: 24.00, 周长: 20.00
}
值接收者 vs 指针接收者
这是一个新手经常困惑的问题:
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 值接收者:操作的是副本
func (p Person) Birthday() {
p.Age++ // 只修改副本,不影响原对象
}
// 指针接收者:操作的是原对象
func (p *Person) BirthdayPtr() {
p.Age++ // 修改原对象
}
func main() {
p := Person{Name: "Alice", Age: 30}
p.Birthday()
fmt.Printf("Birthday后: %s %d岁\n", p.Name, p.Age) // 30岁(未变)
p.BirthdayPtr()
fmt.Printf("BirthdayPtr后: %s %d岁\n", p.Name, p.Age) // 31岁(变了)
}
规则:
- 如果方法需要修改结构体,必须使用指针接收者
- 如果方法只是读取结构体,值接收者和指针接收者都可以
- 混合使用会导致类型不匹配(实现接口的规则不同)
嵌入(组合)——Go的继承替代方案
Go没有继承,但有组合:
package main
import "fmt"
// 基础结构体
type Animal struct {
Name string
}
func (a Animal) Eat() {
fmt.Printf("%s 在吃东西\n", a.Name)
}
func (a Animal) Sleep() {
fmt.Printf("%s 在睡觉\n", a.Name)
}
// 嵌入:Dog 拥有 Animal 的所有方法
type Dog struct {
Animal // 嵌入
Breed string
}
func (d Dog) Bark() {
fmt.Printf("%s 在汪汪叫\n", d.Name)
}
// 嵌入:Cat 拥有 Animal 的所有方法
type Cat struct {
Animal
Color string
}
func (c Cat) Meow() {
fmt.Printf("%s 在喵喵叫\n", c.Name)
}
func main() {
dog := Dog{Animal: Animal{Name: "旺财"}, Breed: "金毛"}
cat := Cat{Animal: Animal{Name: "咪咪"}, Color: "白色"}
dog.Eat() // 旺财 在吃东西(继承自Animal)
dog.Bark() // 旺财 在汪汪叫
cat.Meow() // 咪咪 在喵喵叫
}
第六章:接口——Go的多态核心
Go的接口是隐式实现的,这是它最优雅的设计之一。
基本接口
”`go package main
import (
"fmt"
"math"
)
// 定义接口 type shape interface {
Area() float64
Perimeter() float64
}
// Circle 实现了 shape 接口 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
}
// Rectangle 也实现了 shape 接口 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)
}
// 函数接受接口类型 func describe(s shape) {
fmt.Printf("类型: shape, 面积: %.2f, 周长: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
c := Circle{Radius: 5}
r := Rectangle{Width: 4, Height: 6}
describe(c) //
