Go 语言语法详解 - 像聊天一样学编程
白晓虾 🦐
用生活化的语言,把 Go 语言讲得像聊天一样简单
目录
基础概念
什么是 Go?
Go 就像是一个高效的管家:
- 简单:语法简洁,没有那么多花里胡哨的东西
- 快速:编译速度快,运行速度也快
- 并发:能同时处理很多事情(就像有多个分身)
- 安全:编译时就能发现很多错误
第一个程序
go
package main // 声明包名,main 包是程序的入口
import "fmt" // 导入 fmt 包,用于格式化输出
func main() { // 主函数,程序从这里开始执行
fmt.Println("你好,世界!") // 打印一行文字
}
生活比喻:
package main就像告诉别人"我是这个家的主人"import "fmt"就像"我要用一下打印机工具"func main()就像"开始干活了"fmt.Println()就像"打印出来"
变量与常量
变量 - 会变的盒子
变量就像一个可以换东西的盒子,你可以随时往里面放不同的东西。
声明变量的几种方式
go
package main
import "fmt"
func main() {
// 方式1:完整声明(最正式)
var name string = "白晓虾"
var age int = 18
var isCool bool = true
fmt.Println("名字:", name)
fmt.Println("年龄:", age)
fmt.Println("酷吗:", isCool)
// 方式2:类型推导(让编译器猜)
var city = "北京" // 编译器知道这是字符串
var height = 175 // 编译器知道这是整数
fmt.Println("城市:", city)
fmt.Println("身高:", height)
// 方式3:短变量声明(最常用,像聊天一样简洁)
hobby := "写代码" // := 表示"声明并赋值"
money := 100.5
fmt.Println("爱好:", hobby)
fmt.Println("零花钱:", money)
// 方式4:批量声明(一次声明多个)
var (
username = "xiaoxia"
password = "123456"
email = "xiaoxia@example.com"
)
fmt.Println("用户名:", username)
fmt.Println("密码:", password)
fmt.Println("邮箱:", email)
}
生活场景:
var name string = "白晓虾"就像"我有一个盒子,名字叫 name,专门装字符串,现在里面装着'白晓虾'"hobby := "写代码"就像"我随手拿个盒子叫 hobby,里面先放个'写代码'"
变量的零值
go
package main
import "fmt"
func main() {
// 声明但不赋值,会有默认值(零值)
var name string // 空字符串 ""
var age int // 0
var score float64 // 0.0
var isStudent bool // false
var hobbies []string // nil(空切片)
fmt.Printf("字符串零值: '%s'\n", name)
fmt.Printf("整数零值: %d\n", age)
fmt.Printf("浮点数零值: %f\n", score)
fmt.Printf("布尔值零值: %t\n", isStudent)
fmt.Printf("切片零值: %v\n", hobbies)
}
生活比喻:
- 新买的盒子,没放东西之前,里面就是"空的"或"默认状态"
- 字符串盒子默认是空的("")
- 数字盒子默认是 0
- 布尔盒子默认是 false(没选中)
变量赋值与修改
go
package main
import "fmt"
func main() {
// 声明并赋值
name := "小明"
fmt.Println("初始名字:", name) // 小明
// 修改变量的值
name = "小红"
fmt.Println("修改后:", name) // 小红
// 多重赋值(一次改多个)
a, b := 10, 20
fmt.Println("交换前:", a, b) // 10 20
// 交换两个变量的值(不需要临时变量!)
a, b = b, a
fmt.Println("交换后:", a, b) // 20 10
// 交换的例子:交换两个人的位置
person1, person2 := "左边", "右边"
fmt.Println("换位前:", person1, person2) // 左边 右边
person1, person2 = person2, person1
fmt.Println("换位后:", person1, person2) // 右边 左边
}
常量 - 不会变的盒子
常量就像一个贴了"禁止修改"标签的盒子,一旦放进去就不能换了。
go
package main
import "fmt"
func main() {
// 声明常量
const PI = 3.14159
const MAX_USERS = 1000
const APP_NAME = "我的应用"
fmt.Println("圆周率:", PI)
fmt.Println("最大用户数:", MAX_USERS)
fmt.Println("应用名称:", APP_NAME)
// 批量声明常量
const (
STATUS_ACTIVE = 1 // 活跃
STATUS_INACTIVE = 0 // 不活跃
STATUS_BANNED = -1 // 被封禁
)
fmt.Println("活跃状态:", STATUS_ACTIVE)
fmt.Println("不活跃状态:", STATUS_INACTIVE)
// iota - 自动递增的常量生成器
const (
MONDAY = iota // 0
TUESDAY // 1
WEDNESDAY // 2
THURSDAY // 3
FRIDAY // 4
SATURDAY // 5
SUNDAY // 6
)
fmt.Println("星期一:", MONDAY) // 0
fmt.Println("星期五:", FRIDAY) // 4
// iota 的高级用法
const (
_ = iota // 0(跳过)
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
)
fmt.Println("1KB:", KB) // 1024
fmt.Println("1MB:", MB) // 1048576
fmt.Println("1GB:", GB) // 1073741824
}
生活场景:
const PI = 3.14159就像"这个盒子永远装 3.14159,谁也不能改"iota就像"自动编号器",从 0 开始,每个自动加 1
数据类型
基本类型
go
package main
import "fmt"
func main() {
// ============ 字符串 ============
name := "白晓虾"
greeting := `多行字符串
可以这样写
不用转义`
fmt.Println("名字:", name)
fmt.Println("问候:", greeting)
// 字符串拼接
fullName := "白" + "晓" + "虾"
fmt.Println("全名:", fullName)
// ============ 整数 ============
var age int = 18 // 根据系统自动选择 int32 或 int64
var smallNumber int8 = 127 // -128 到 127
var bigNumber int64 = 9999999999
var unsigned uint = 100 // 无符号整数(只能是正数)
var byteVal uint8 = 255 // uint8 的别名,0-255
fmt.Println("年龄:", age)
fmt.Println("小数字:", smallNumber)
fmt.Println("大数字:", bigNumber)
fmt.Println("无符号数:", unsigned)
// ============ 浮点数 ============
var price float32 = 19.99
var pi float64 = 3.141592653589793
fmt.Println("价格:", price)
fmt.Println("圆周率:", pi)
// ============ 布尔值 ============
var isStudent bool = true
var isAdult bool = false
fmt.Println("是学生:", isStudent)
fmt.Println("是成年人:", isAdult)
// 布尔运算
fmt.Println("true && false:", true && false) // false(与)
fmt.Println("true || false:", true || false) // true(或)
fmt.Println("!true:", !true) // false(非)
}
生活比喻:
string就像"文字盒子",只能装文字int就像"整数盒子",只能装整数float就像"小数盒子",可以装小数bool就像"开关盒子",只有开和关两个状态
类型转换
Go 是强类型语言,不会自动转换类型,必须手动转换。
go
package main
import "fmt"
func main() {
// 整数转浮点数
var age int = 18
var ageFloat float64 = float64(age)
fmt.Println("年龄(整):", age)
fmt.Println("年龄(浮):", ageFloat)
// 浮点数转整数(会丢失小数部分)
var price float64 = 19.99
var priceInt int = int(price)
fmt.Println("价格(浮):", price)
fmt.Println("价格(整):", priceInt) // 19(小数部分丢失)
// 数字转字符串
var score int = 95
var scoreStr string = fmt.Sprintf("%d", score)
fmt.Println("分数(数字):", score)
fmt.Println("分数(字符串):", scoreStr)
// 字符串转数字
var strNumber string = "123"
var number int
fmt.Sscanf(strNumber, "%d", &number)
fmt.Println("字符串:", strNumber)
fmt.Println("数字:", number)
// 注意:不能直接把字符串当数字用
// var x int = "123" // ❌ 编译错误
var x int = 123 // ✅ 正确
fmt.Println("x =", x)
}
生活场景:
- 类型转换就像"把东西从一个盒子拿出来,放到另一个盒子里"
- 但要注意:有些转换会"丢失信息",比如小数转整数会丢掉小数部分
运算符
算术运算符
go
package main
import "fmt"
func main() {
a := 10
b := 3
// 基本运算
fmt.Println("加法:", a + b) // 13
fmt.Println("减法:", a - b) // 7
fmt.Println("乘法:", a * b) // 30
fmt.Println("除法:", a / b) // 3(整数除法,舍去小数)
fmt.Println("取余:", a % b) // 1
// 浮点数除法
fmt.Println("10.0 / 3.0 =", 10.0 / 3.0) // 3.333...
// 自增和自减(只能作为语句,不能用于表达式)
x := 5
x++ // x = x + 1
fmt.Println("x++ 后:", x) // 6
x-- // x = x - 1
fmt.Println("x-- 后:", x) // 5
// ❌ 不能这样用
// y := x++ // 编译错误
// if x++ > 5 { } // 编译错误
// 复合赋值运算符
x += 5 // x = x + 5
fmt.Println("x += 5:", x) // 10
x -= 3 // x = x - 3
fmt.Println("x -= 3:", x) // 7
x *= 2 // x = x * 2
fmt.Println("x *= 2:", x) // 14
x /= 7 // x = x / 7
fmt.Println("x /= 7:", x) // 2
}
生活场景:
+ - * /就像日常的加减乘除%就像"分蛋糕,看剩几块"++和--就像"给自己加一块"或"减一块"
比较运算符
go
package main
import "fmt"
func main() {
a := 10
b := 20
fmt.Println("a == b:", a == b) // false(等于)
fmt.Println("a != b:", a != b) // true(不等于)
fmt.Println("a < b:", a < b) // true(小于)
fmt.Println("a <= b:", a <= b) // true(小于等于)
fmt.Println("a > b:", a > b) // false(大于)
fmt.Println("a >= b:", a >= b) // false(大于等于)
// 字符串比较(按字典序)
str1 := "apple"
str2 := "banana"
fmt.Println("apple < banana:", str1 < str2) // true
}
逻辑运算符
go
package main
import "fmt"
func main() {
a := true
b := false
// 与(&&):两个都为 true 才是 true
fmt.Println("true && false:", a && b) // false
fmt.Println("true && true:", a && a) // true
// 或(||):有一个为 true 就是 true
fmt.Println("true || false:", a || b) // true
fmt.Println("false || false:", b || b) // false
// 非(!):取反
fmt.Println("!true:", !a) // false
fmt.Println("!false:", !b) // true
// 短路求值(重要!)
// &&:如果第一个是 false,不会计算第二个
// ||:如果第一个是 true,不会计算第二个
// 例子:检查年龄是否在 18-60 之间
age := 25
isAdult := age >= 18 && age <= 60
fmt.Println("年龄 25 是否在 18-60 之间:", isAdult) // true
// 例子:检查是否是周末
day := 6 // 假设 6 是周六
isWeekend := day == 6 || day == 7
fmt.Println("是否是周末:", isWeekend) // true
}
生活场景:
&&就像"两个条件都要满足"||就像"满足其中一个就行"!就像"反过来"
流程控制
if-else - 如果...就...
go
package main
import "fmt"
func main() {
// 基本的 if
age := 18
if age >= 18 {
fmt.Println("你已经成年了")
}
// if-else
if age >= 18 {
fmt.Println("可以考驾照")
} else {
fmt.Println("还不能考驾照")
}
// if-else if-else
score := 85
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
// if 的特殊写法:可以在条件前声明变量
if num := 10; num%2 == 0 {
fmt.Printf("%d 是偶数\n", num)
} else {
fmt.Printf("%d 是奇数\n", num)
}
// 例子:判断天气
weather := "晴天"
if weather == "晴天" {
fmt.Println("适合出去玩")
} else if weather == "雨天" {
fmt.Println("记得带伞")
} else {
fmt.Println("随便吧")
}
}
生活场景:
if就像"如果...,就..."else就像"否则..."else if就像"或者如果..."
switch - 多选一
go
package main
import "fmt"
func main() {
// 基本的 switch
day := 3
switch day {
case 1:
fmt.Println("星期一")
case 2:
fmt.Println("星期二")
case 3:
fmt.Println("星期三")
case 4:
fmt.Println("星期四")
case 5:
fmt.Println("星期五")
case 6:
fmt.Println("星期六")
case 7:
fmt.Println("星期日")
default:
fmt.Println("未知的日子")
}
// 一个 case 可以有多个值
month := 7
switch month {
case 12, 1, 2:
fmt.Println("冬天")
case 3, 4, 5:
fmt.Println("春天")
case 6, 7, 8:
fmt.Println("夏天")
case 9, 10, 11:
fmt.Println("秋天")
default:
fmt.Println("未知季节")
}
// 不带条件的 switch(相当于 if-else if-else)
score := 85
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
// fallthrough:继续执行下一个 case(不常用)
num := 1
switch num {
case 1:
fmt.Println("这是 1")
fallthrough // 继续执行 case 2
case 2:
fmt.Println("这是 2")
default:
fmt.Println("其他")
}
// 输出:
// 这是 1
// 这是 2
}
生活场景:
switch就像"多选一"的菜单case就是每个选项default就是"以上都不是"fallthrough就像"这个选了,顺便把下一个也选了"
for - 循环
Go 只有 for 循环,没有 while 和 do-while。
go
package main
import "fmt"
func main() {
// ============ 形式1:类似 while ============
i := 0
for i < 5 {
fmt.Println("计数:", i)
i++
}
// ============ 形式2:经典的 for 循环 ============
for j := 0; j < 5; j++ {
fmt.Println("j =", j)
}
// ============ 形式3:无限循环 ============
count := 0
for {
fmt.Println("无限循环中...")
count++
if count >= 3 {
break // 跳出循环
}
}
// ============ 遍历数组/切片 ============
fruits := []string{"苹果", "香蕉", "橙子"}
// 方式1:只要值
for _, fruit := range fruits {
fmt.Println("水果:", fruit)
}
// 方式2:要索引和值
for i, fruit := range fruits {
fmt.Printf("第 %d 个水果: %s\n", i, fruit)
}
// 方式3:只要索引
for i := range fruits {
fmt.Println("索引:", i)
}
// ============ 遍历 map ============
scores := map[string]int{
"语文": 90,
"数学": 95,
"英语": 88,
}
for subject, score := range scores {
fmt.Printf("%s: %d 分\n", subject, score)
}
// ============ break 和 continue ============
// break:跳出整个循环
fmt.Println("\nbreak 示例:")
for i := 0; i < 10; i++ {
if i == 5 {
break // 遇到 5 就停止
}
fmt.Println(i)
}
// 输出:0 1 2 3 4
// continue:跳过本次循环,继续下一次
fmt.Println("\ncontinue 示例:")
for i := 0; i < 5; i++ {
if i == 2 {
continue // 跳过 2
}
fmt.Println(i)
}
// 输出:0 1 3 4
// ============ 嵌套循环 + 标签 ============
fmt.Println("\n嵌套循环示例:")
OuterLoop: // 给外层循环起个名字
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
fmt.Println("跳过外层循环")
break OuterLoop // 跳出外层循环
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
}
生活场景:
for就像"重复做某事"break就像"不干了,停下来"continue就像"这次算了,下次继续"- 嵌套循环就像"做套娃,一层套一层"
函数
函数基础
函数就像一个"工具箱",你把东西放进去,它处理后给你结果。
go
package main
import "fmt"
// 无参数无返回值的函数
func sayHello() {
fmt.Println("你好!")
}
// 有参数无返回值的函数
func greet(name string) {
fmt.Printf("你好,%s!\n", name)
}
// 有参数有返回值的函数
func add(a int, b int) int {
return a + b
}
// 多个返回值(Go 的特色!)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("不能除以 0")
}
return a / b, nil
}
// 命名返回值(返回时不用写变量名)
func rectangle(width, height float64) (area, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // 直接返回 area 和 perimeter
}
// 可变参数函数(参数个数不固定)
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
// 调用函数
sayHello()
greet("白晓虾")
result := add(10, 20)
fmt.Println("10 + 20 =", result)
// 多返回值
quotient, err := divide(10, 2)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("10 / 2 =", quotient)
}
quotient, err = divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
}
// 命名返回值
area, perimeter := rectangle(5, 3)
fmt.Printf("面积: %.2f, 周长: %.2f\n", area, perimeter)
// 可变参数
fmt.Println("1+2+3 =", sum(1, 2, 3))
fmt.Println("1+2+3+4+5 =", sum(1, 2, 3, 4, 5))
// 也可以传切片
nums := []int{10, 20, 30}
fmt.Println("10+20+30 =", sum(nums...))
}
生活场景:
- 函数就像"洗衣机":你把衣服放进去(参数),它洗完给你干净衣服(返回值)
- 多返回值就像"洗衣机洗完衣服,顺便把水也排了"
- 可变参数就像"不管你放多少衣服,我都能洗"
函数作为值
在 Go 中,函数也是一种类型,可以像变量一样传递。
go
package main
import "fmt"
// 函数类型
type operation func(int, int) int
// 接收函数作为参数
func calculate(a, b int, op operation) int {
return op(a, b)
}
// 返回函数
func getOperation(opType string) operation {
switch opType {
case "add":
return func(a, b int) int {
return a + b
}
case "multiply":
return func(a, b int) int {
return a * b
}
default:
return func(a, b int) int {
return 0
}
}
}
func main() {
// 函数作为变量
add := func(a, b int) int {
return a + b
}
result := add(10, 20)
fmt.Println("10 + 20 =", result)
// 函数作为参数
sum := calculate(10, 20, add)
fmt.Println("计算结果:", sum)
// 匿名函数
multiply := func(a, b int) int {
return a * b
}
product := calculate(5, 6, multiply)
fmt.Println("5 * 6 =", product)
// 函数作为返回值
op := getOperation("add")
fmt.Println("getOperation('add')(10, 20) =", op(10, 20))
op = getOperation("multiply")
fmt.Println("getOperation('multiply')(5, 6) =", op(5, 6))
}
生活场景:
- 函数作为值就像"工具箱本身也可以被传递"
- 你可以把"锤子"给别人用,也可以让别人给你"锤子"
defer - 延迟执行
defer 会在函数返回前执行,常用于资源清理。
go
package main
import "fmt"
func main() {
// defer 会延迟执行,直到函数结束
defer fmt.Println("1. 这是 defer 语句")
defer fmt.Println("2. 这也是 defer 语句")
fmt.Println("3. 普通语句")
// 执行顺序:3 -> 2 -> 1(后进先出)
fmt.Println("\n--- defer 的实际应用 ---")
// 例子:打开文件后关闭
// 假设这是打开文件
file := "文件已打开"
defer fmt.Println("关闭文件:", file) // 确保文件会被关闭
fmt.Println("读取文件内容...")
fmt.Println("处理数据...")
// 即使中间出错,defer 也会执行
// 例子:记录函数执行时间
fmt.Println("\n--- 计时示例 ---")
defer func() {
fmt.Println("函数执行完毕")
}()
fmt.Println("开始执行...")
fmt.Println("执行中...")
fmt.Println("快完成了...")
}
生活场景:
defer就像"出门前最后检查一遍:关灯、锁门、带钥匙"- 不管中间发生了什么,最后都要做这些事
数组与切片
数组 - 固定大小的盒子
数组就像一排固定数量的盒子,每个盒子都有编号。
go
package main
import "fmt"
func main() {
// 声明数组(指定长度)
var arr1 [5]int // 5 个整数,初始值都是 0
fmt.Println("arr1:", arr1) // [0 0 0 0 0]
// 声明并初始化
arr2 := [5]int{1, 2, 3, 4, 5}
fmt.Println("arr2:", arr2) // [1 2 3 4 5]
// 让编译器数长度
arr3 := [...]int{10, 20, 30}
fmt.Println("arr3:", arr3) // [10 20 30]
// 指定索引初始化
arr4 := [5]int{1: 100, 3: 300}
fmt.Println("arr4:", arr4) // [0 100 0 300 0]
// 访问和修改元素
arr := [3]string{"苹果", "香蕉", "橙子"}
fmt.Println("第一个水果:", arr[0]) // 苹果
fmt.Println("最后一个水果:", arr[len(arr)-1]) // 橙子
arr[1] = "葡萄" // 修改第二个元素
fmt.Println("修改后:", arr) // [苹果 葡萄 橙子]
// 遍历数组
fmt.Println("\n遍历数组:")
for i, fruit := range arr {
fmt.Printf("索引 %d: %s\n", i, fruit)
}
// 数组长度
fmt.Println("\n数组长度:", len(arr))
// 数组比较(相同类型和长度才能比较)
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println("a == b:", a == b) // true
fmt.Println("a == c:", a == c) // false
}
生活场景:
- 数组就像"一排固定数量的储物柜"
- 每个柜子都有编号(索引),从 0 开始
- 一旦建好,柜子数量就不能改了
切片 - 动态的盒子
切片就像"可以随时增减的盒子",更灵活。
go
package main
import "fmt"
func main() {
// ============ 创建切片 ============
// 方式1:直接创建
fruits := []string{"苹果", "香蕉", "橙子"}
fmt.Println("水果:", fruits)
// 方式2:用 make 创建(指定长度和容量)
nums := make([]int, 5) // 长度 5,容量 5
fmt.Println("nums:", nums) // [0 0 0 0 0]
// 方式3:从数组切出来
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // 取索引 1 到 3(不包括 4)
fmt.Println("切片:", slice) // [2 3 4]
// ============ 切片操作 ============
// 添加元素
fruits = append(fruits, "葡萄")
fmt.Println("添加后:", fruits) // [苹果 香蕉 橙子 葡萄]
// 添加多个元素
fruits = append(fruits, "西瓜", "草莓")
fmt.Println("再添加:", fruits)
// 合并切片
moreFruits := []string{"桃子", "李子"}
fruits = append(fruits, moreFruits...)
fmt.Println("合并后:", fruits)
// ============ 切片的长度和容量 ============
s := make([]int, 3, 5) // 长度 3,容量 5
fmt.Printf("长度: %d, 容量: %d\n", len(s), cap(s))
// ============ 切片的切片 ============
original := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 取前 5 个
first5 := original[:5]
fmt.Println("前 5 个:", first5)
// 取后 5 个
last5 := original[5:]
fmt.Println("后 5 个:", last5)
// 取中间 3 个
middle := original[3:6]
fmt.Println("中间 3 个:", middle)
// ============ 复制切片 ============
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a)
fmt.Println("原切片:", a)
fmt.Println("复制的:", b)
// 修改 b 不会影响 a
b[0] = 999
fmt.Println("修改 b 后:")
fmt.Println("a:", a) // [1 2 3]
fmt.Println("b:", b) // [999 2 3]
// ============ 删除元素 ============
// 删除索引为 2 的元素
items := []string{"a", "b", "c", "d", "e"}
index := 2
items = append(items[:index], items[index+1:]...)
fmt.Println("删除索引 2 后:", items) // [a b d e]
// ============ 切片的底层 ============
// 切片是对数组的引用
arr2 := [5]int{10, 20, 30, 40, 50}
slice1 := arr2[1:4] // [20 30 40]
slice2 := arr2[2:5] // [30 40 50]
// 修改 slice1 会影响 slice2(因为它们共享底层数组)
slice1[1] = 999
fmt.Println("slice1:", slice1) // [20 999 40]
fmt.Println("slice2:", slice2) // [999 40 50]
fmt.Println("arr2:", arr2) // [10 20 999 40 50]
}
生活场景:
- 切片就像"弹性储物柜",可以随时增加或减少柜子
append就像"再加一个柜子"- 切片的切片就像"从储物柜里拿出一部分,但它们还是连着的"
映射(Map)
Map 就像一本字典,你可以通过"键"找到对应的"值"。
go
package main
import "fmt"
func main() {
// ============ 创建 map ============
// 方式1:直接创建
person := map[string]string{
"name": "白晓虾",
"city": "北京",
"job": "程序员",
}
fmt.Println("个人信息:", person)
// 方式2:用 make 创建
scores := make(map[string]int)
scores["语文"] = 90
scores["数学"] = 95
scores["英语"] = 88
fmt.Println("成绩:", scores)
// ============ 访问和修改 ============
// 访问
fmt.Println("姓名:", person["name"]) // 白晓虾
fmt.Println("城市:", person["city"]) // 北京
// 修改
person["city"] = "上海"
fmt.Println("修改后的城市:", person["city"])
// 添加新键值对
person["age"] = "18"
fmt.Println("添加年龄后:", person)
// ============ 检查键是否存在 ============
// 使用 "comma ok" 模式
value, exists := person["name"]
if exists {
fmt.Println("姓名:", value)
} else {
fmt.Println("没有找到姓名")
}
// 检查不存在的键
value, exists = person["hobby"]
if exists {
fmt.Println("爱好:", value)
} else {
fmt.Println("没有找到爱好")
}
// ============ 删除键值对 ============
delete(person, "age")
fmt.Println("删除年龄后:", person)
// ============ 遍历 map ============
fmt.Println("\n遍历成绩:")
for subject, score := range scores {
fmt.Printf("%s: %d 分\n", subject, score)
}
// 只遍历键
fmt.Println("\n所有科目:")
for subject := range scores {
fmt.Println(subject)
}
// ============ map 的长度 ============
fmt.Println("\n成绩数量:", len(scores))
// ============ 嵌套 map ============
students := map[string]map[string]int{
"小明": {
"语文": 90,
"数学": 95,
},
"小红": {
"语文": 88,
"数学": 92,
},
}
fmt.Println("\n学生成绩:")
for name, subjects := range students {
fmt.Printf("%s: ", name)
for subject, score := range subjects {
fmt.Printf("%s=%d ", subject, score)
}
fmt.Println()
}
// ============ 注意事项 ============
// map 是引用类型
m1 := map[string]int{"a": 1, "b": 2}
m2 := m1
m2["a"] = 999
fmt.Println("\nm1:", m1) // map[a:999 b:2] - m1 也被修改了!
fmt.Println("m2:", m2) // map[a:999 b:2]
// map 的零值是 nil
var nilMap map[string]int
fmt.Println("\nnilMap 是否为 nil:", nilMap == nil) // true
// nil map 不能直接使用
// nilMap["key"] = 1 // ❌ 运行时错误!
// 需要先初始化
nilMap = make(map[string]int)
nilMap["key"] = 1 // ✅ 现在可以了
fmt.Println("nilMap 初始化后:", nilMap)
}
生活场景:
- Map 就像"电话簿",通过名字找到电话号码
map[key]就像"查电话簿"delete(map, key)就像"撕掉那一页"- Map 是引用类型,就像"两个人用同一本电话簿"
结构体
结构体就像一个自定义的盒子,可以装不同类型的东西。
go
package main
import "fmt"
// 定义结构体
type Person struct {
Name string // 大写开头表示公开
Age int
City string
}
// 结构体方法(值接收者)
func (p Person) Introduce() string {
return fmt.Sprintf("我是 %s,今年 %d 岁,住在 %s", p.Name, p.Age, p.City)
}
// 结构体方法(指针接收者,可以修改结构体)
func (p *Person) Birthday() {
p.Age++
fmt.Printf("%s 过生日了!现在 %d 岁\n", p.Name, p.Age)
}
// 结构体嵌套
type Address struct {
Province string
City string
Street string
}
type Employee struct {
Name string
Age int
Address Address // 嵌套结构体
}
// 匿名结构体
func getPoint() struct {
X int
Y int
} {
return struct {
X int
Y int
}{X: 10, Y: 20}
}
func main() {
// ============ 创建结构体实例 ============
// 方式1:完整写法
p1 := Person{
Name: "白晓虾",
Age: 18,
City: "北京",
}
fmt.Println("p1:", p1)
// 方式2:按顺序(不推荐,容易出错)
p2 := Person{"小红", 20, "上海"}
fmt.Println("p2:", p2)
// 方式3:部分初始化
p3 := Person{Name: "小明"}
fmt.Println("p3:", p3) // {小明 0 }
// 方式4:new 函数(返回指针)
p4 := new(Person)
p4.Name = "小李"
p4.Age = 25
fmt.Println("p4:", *p4)
// ============ 访问和修改 ============
fmt.Println("\n访问字段:")
fmt.Println("姓名:", p1.Name)
fmt.Println("年龄:", p1.Age)
p1.Age = 19
fmt.Println("修改后的年龄:", p1.Age)
// ============ 结构体方法 ============
fmt.Println("\n调用方法:")
intro := p1.Introduce()
fmt.Println(intro)
p1.Birthday() // 年龄变成 20
// ============ 结构体指针 ============
p5 := &Person{Name: "小王", Age: 30}
fmt.Println("\np5:", *p5)
p5.Birthday() // 指针接收者自动解引用
// ============ 结构体嵌套 ============
emp := Employee{
Name: "张三",
Age: 28,
Address: Address{
Province: "北京",
City: "北京市",
Street: "长安街",
},
}
fmt.Println("\n员工信息:")
fmt.Printf("姓名: %s\n", emp.Name)
fmt.Printf("地址: %s %s %s\n", emp.Address.Province, emp.Address.City, emp.Address.Street)
// ============ 匿名结构体 ============
point := struct {
X int
Y int
}{
X: 100,
Y: 200,
}
fmt.Println("\n坐标:", point)
// ============ 结构体比较 ============
a := Person{Name: "小明", Age: 20, City: "北京"}
b := Person{Name: "小明", Age: 20, City: "北京"}
c := Person{Name: "小红", Age: 20, City: "北京"}
fmt.Println("\n结构体比较:")
fmt.Println("a == b:", a == b) // true
fmt.Println("a == c:", a == c) // false
// ============ 结构体切片 ============
people := []Person{
{Name: "小明", Age: 20, City: "北京"},
{Name: "小红", Age: 22, City: "上海"},
{Name: "小李", Age: 25, City: "广州"},
}
fmt.Println("\n人员列表:")
for i, p := range people {
fmt.Printf("%d. %s (%d岁, %s)\n", i+1, p.Name, p.Age, p.City)
}
// ============ 结构体工厂函数 ============
newPerson := func(name string, age int, city string) *Person {
return &Person{
Name: name,
Age: age,
City: city,
}
}
p6 := newPerson("小赵", 30, "深圳")
fmt.Println("\n工厂函数创建:", *p6)
}
生活场景:
- 结构体就像"名片",上面有姓名、年龄、地址等信息
- 结构体方法就像"名片主人能做的事"
- 结构体嵌套就像"名片夹里还有小名片"
指针
指针就像一个地址标签,告诉你东西在哪里,而不是东西本身。
go
package main
import "fmt"
func main() {
// ============ 指针基础 ============
a := 10
// & 取地址
p := &a // p 是指向 a 的指针
fmt.Println("a 的值:", a) // 10
fmt.Println("a 的地址:", &a) // 0x...
fmt.Println("p 的值:", p) // 0x...(和 &a 一样)
fmt.Println("p 指向的值:", *p) // 10(* 是解引用)
// 通过指针修改值
*p = 20
fmt.Println("\n通过指针修改后:")
fmt.Println("a 的值:", a) // 20
fmt.Println("*p 的值:", *p) // 20
// ============ 指针和函数 ============
// 值传递(不会修改原值)
x := 10
fmt.Println("\n值传递:")
fmt.Println("修改前 x:", x)
modifyByValue(x)
fmt.Println("修改后 x:", x) // 还是 10
// 指针传递(会修改原值)
fmt.Println("\n指针传递:")
fmt.Println("修改前 x:", x)
modifyByPointer(&x)
fmt.Println("修改后 x:", x) // 变成 100
// ============ 指针和结构体 ============
type Person struct {
Name string
Age int
}
p1 := Person{Name: "小明", Age: 20}
fmt.Println("\n结构体指针:")
fmt.Println("修改前:", p1)
// 指针接收者可以修改结构体
modifyPerson(&p1)
fmt.Println("修改后:", p1)
// ============ new 函数 ============
// new 创建指针并初始化为零值
p2 := new(int) // *int,值为 0
p3 := new(string) // *string,值为 ""
fmt.Println("\nnew 函数:")
fmt.Println("*p2:", *p2) // 0
fmt.Println("*p3:", *p3) // ""
*p2 = 100
fmt.Println("赋值后 *p2:", *p2)
// ============ 指针数组 ============
arr := [3]int{1, 2, 3}
pArr := &arr
fmt.Println("\n指针数组:")
fmt.Println("arr:", arr)
fmt.Println("*pArr:", *pArr)
fmt.Println("(*pArr)[0]:", (*pArr)[0])
// ============ 指针的指针 ============
val := 10
p1_ := &val
p2_ := &p1_ // 指针的指针
fmt.Println("\n指针的指针:")
fmt.Println("val:", val)
fmt.Println("*p1_:", *p1_) // 10
fmt.Println("**p2_:", **p2_) // 10
// ============ 指针注意事项 ============
// Go 没有指针运算(不像 C 语言)
// p++ 是错误的
// nil 指针
var nilPointer *int
fmt.Println("\nnil 指针:", nilPointer) // <nil>
// 使用前要检查
if nilPointer == nil {
fmt.Println("指针是 nil,不能解引用")
}
}
func modifyByValue(n int) {
n = 100 // 修改的是副本
}
func modifyByPointer(n *int) {
*n = 100 // 修改的是原值
}
func modifyPerson(p *struct {
Name string
Age int
}) {
p.Name = "小红"
p.Age = 25
}
生活场景:
- 指针就像"门牌号",告诉你东西在哪里
&就像"告诉我地址"*就像"去那个地址拿东西"- 值传递就像"复印一份给别人"
- 指针传递就像"告诉别人地址,让他自己去取"
接口
接口就像一个约定,规定了一组方法,任何实现了这些方法的类型都满足这个接口。
go
package main
import (
"fmt"
"math"
)
// ============ 定义接口 ============
// 形状接口
type Shape interface {
Area() float64
Perimeter() float64
}
// 描述接口
type Describer interface {
Describe() string
}
// ============ 实现接口 ============
// 圆形
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
}
func (c Circle) Describe() string {
return fmt.Sprintf("圆形,半径 %.2f", c.Radius)
}
// 矩形
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func (r Rectangle) Describe() string {
return fmt.Sprintf("矩形,宽 %.2f,高 %.2f", r.Width, r.Height)
}
// ============ 使用接口 ============
// 接受接口作为参数的函数
func PrintShapeInfo(s Shape) {
fmt.Printf("面积: %.2f, 周长: %.2f\n", s.Area(), s.Perimeter())
}
func PrintDescription(d Describer) {
fmt.Println(d.Describe())
}
// ============ 空接口 ============
// interface{} 可以接受任何类型
func PrintAny(v interface{}) {
fmt.Printf("值: %v, 类型: %T\n", v, v)
}
// ============ 类型断言 ============
func GetType(s Shape) {
switch v := s.(type) {
case Circle:
fmt.Printf("这是一个圆形,半径 %.2f\n", v.Radius)
case Rectangle:
fmt.Printf("这是一个矩形,宽 %.2f,高 %.2f\n", v.Width, v.Height)
default:
fmt.Println("未知形状")
}
}
func main() {
// 创建形状
circle := Circle{Radius: 5}
rectangle := Rectangle{Width: 4, Height: 3}
fmt.Println("=== 圆形 ===")
PrintShapeInfo(circle)
PrintDescription(circle)
fmt.Println("\n=== 矩形 ===")
PrintShapeInfo(rectangle)
PrintDescription(rectangle)
// 接口切片
fmt.Println("\n=== 形状列表 ===")
shapes := []Shape{circle, rectangle}
for i, shape := range shapes {
fmt.Printf("形状 %d: ", i+1)
PrintShapeInfo(shape)
}
// 空接口
fmt.Println("\n=== 空接口 ===")
PrintAny(42)
PrintAny("hello")
PrintAny(3.14)
PrintAny([]int{1, 2, 3})
// 类型断言
fmt.Println("\n=== 类型断言 ===")
GetType(circle)
GetType(rectangle)
// 类型断言(带检查)
var s Shape = circle
if c, ok := s.(Circle); ok {
fmt.Printf("确认是圆形,半径: %.2f\n", c.Radius)
}
// ============ 接口组合 ============
// 组合多个接口
type ShapeDescriber interface {
Shape
Describer
}
var sd ShapeDescriber = circle
fmt.Println("\n=== 接口组合 ===")
fmt.Printf("面积: %.2f\n", sd.Area())
fmt.Println(sd.Describe())
// ============ 接口零值 ============
var nilShape Shape
fmt.Println("\n=== 接口零值 ===")
fmt.Println("nilShape == nil:", nilShape == nil) // true
// 注意:接口包含类型和值两部分
// 只有类型和值都是 nil,接口才等于 nil
var p *Circle // p 是 nil
var s2 Shape = p
fmt.Println("p == nil:", p == nil) // true
fmt.Println("s2 == nil:", s2 == nil) // false!s2 有类型信息
}
生活场景:
- 接口就像"工作合同",规定了要做什么事
- 实现接口就像"签合同",保证会做这些事
- 空接口就像"万能容器",什么都能装
- 类型断言就像"检查里面到底是什么"
错误处理
Go 的错误处理很直接:函数返回错误,调用者检查错误。
go
package main
import (
"errors"
"fmt"
)
// ============ 自定义错误 ============
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("错误 %d: %s", e.Code, e.Message)
}
// ============ 返回错误的函数 ============
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, &MyError{
Code: 1001,
Message: "不能除以零",
}
}
return a / b, nil
}
func GetUser(id int) (string, error) {
if id <= 0 {
return "", errors.New("用户 ID 必须大于 0")
}
if id > 100 {
return "", fmt.Errorf("用户 %d 不存在", id)
}
return fmt.Sprintf("用户_%d", id), nil
}
// ============ panic 和 recover ============
func SafeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("发生 panic: %v", r)
}
}()
if b == 0 {
panic("除数不能为零")
}
return a / b, nil
}
func main() {
// ============ 基本错误处理 ============
result, err := Divide(10, 2)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
result, err = Divide(10, 0)
if err != nil {
fmt.Println("错误:", err) // 错误 1001: 不能除以零
}
// ============ 错误类型断言 ============
if myErr, ok := err.(*MyError); ok {
fmt.Printf("自定义错误 - 代码: %d, 消息: %s\n", myErr.Code, myErr.Message)
}
// ============ 错误包装 ============
user, err := GetUser(-1)
if err != nil {
fmt.Println("\n获取用户错误:", err)
}
user, err = GetUser(200)
if err != nil {
fmt.Println("获取用户错误:", err)
}
user, err = GetUser(50)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("用户:", user)
}
// ============ panic 和 recover ============
fmt.Println("\n=== panic 和 recover ===")
result2, err := SafeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result2)
}
result2, err = SafeDivide(10, 2)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result2)
}
// ============ 错误处理最佳实践 ============
fmt.Println("\n=== 错误处理最佳实践 ===")
// 1. 不要忽略错误
// val, _ := someFunc() // ❌ 不好
// val, err := someFunc() // ✅ 好
// if err != nil { ... }
// 2. 尽早返回
// if err != nil {
// return err // 尽早返回,减少嵌套
// }
// 3. 添加上下文
// if err != nil {
// return fmt.Errorf("处理用户 %d 时出错: %w", userID, err)
// }
fmt.Println("错误处理示例完成")
}
生活场景:
- 错误就像"快递没送到",需要检查并处理
panic就像"紧急情况",程序崩溃recover就像"急救",把程序救回来defer就像"最后检查",确保资源被释放
并发
并发是 Go 的强项,用 goroutine 和 channel 轻松实现。
Goroutine - 轻量级线程
go
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("%s: 你好 %d\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// ============ 启动 goroutine ============
// 普通函数调用(同步)
fmt.Println("=== 同步调用 ===")
sayHello("主线程")
// 启动 goroutine(异步)
fmt.Println("\n=== 异步调用 ===")
go sayHello("协程1")
go sayHello("协程2")
go sayHello("协程3")
// 主函数要等待,否则 goroutine 还没执行完程序就退出了
time.Sleep(500 * time.Millisecond)
// ============ 匿名函数 goroutine ============
fmt.Println("\n=== 匿名函数 goroutine ===")
go func(msg string) {
fmt.Println(msg)
}("这是匿名 goroutine")
time.Sleep(100 * time.Millisecond)
// ============ goroutine 数量 ============
fmt.Println("\n=== 启动多个 goroutine ===")
for i := 0; i < 5; i++ {
go func(id int) {
fmt.Printf("Goroutine %d\n", id)
}(i) // 注意:要把 i 作为参数传入
}
time.Sleep(200 * time.Millisecond)
// ============ WaitGroup 等待 goroutine ============
fmt.Println("\n=== WaitGroup ===")
// 使用 sync.WaitGroup 等待所有 goroutine 完成
// 见下面的示例
}
Channel - 通道
go
package main
import (
"fmt"
"time"
)
func main() {
// ============ 创建 channel ============
// 无缓冲 channel
ch := make(chan string)
// 发送和接收
go func() {
time.Sleep(100 * time.Millisecond)
ch <- "你好" // 发送
}()
msg := <-ch // 接收(会阻塞直到收到消息)
fmt.Println("收到:", msg)
// ============ 有缓冲 channel ============
fmt.Println("\n=== 有缓冲 channel ===")
bufferedCh := make(chan int, 3) // 缓冲区大小为 3
bufferedCh <- 1 // 不阻塞
bufferedCh <- 2 // 不阻塞
bufferedCh <- 3 // 不阻塞
// bufferedCh <- 4 // 阻塞!缓冲区满了
fmt.Println("缓冲区大小:", len(bufferedCh), "容量:", cap(bufferedCh))
fmt.Println("接收:", <-bufferedCh) // 1
fmt.Println("接收:", <-bufferedCh) // 2
fmt.Println("接收:", <-bufferedCh) // 3
// ============ 关闭 channel ============
fmt.Println("\n=== 关闭 channel ===")
jobs := make(chan int, 5)
done := make(chan bool)
// 消费者
go func() {
for {
job, more := <-jobs
if more {
fmt.Println("处理任务:", job)
} else {
fmt.Println("所有任务完成")
done <- true
return
}
}
}()
// 生产者
for i := 1; i <= 3; i++ {
jobs <- i
}
close(jobs) // 关闭 channel
<-done // 等待完成
// ============ range channel ============
fmt.Println("\n=== range channel ===")
nums := make(chan int, 3)
nums <- 1
nums <- 2
nums <- 3
close(nums)
for n := range nums {
fmt.Println("收到:", n)
}
// ============ select 多路复用 ============
fmt.Println("\n=== select ===")
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "来自 ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "来自 ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(300 * time.Millisecond):
fmt.Println("超时")
}
}
// ============ 单向 channel ============
fmt.Println("\n=== 单向 channel ===")
// 只发送
sender := func(ch chan<- int) {
ch <- 42
}
// 只接收
receiver := func(ch <-chan int) {
fmt.Println("收到:", <-ch)
}
ch3 := make(chan int, 1)
sender(ch3)
receiver(ch3)
}
sync 包 - 同步原语
go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// ============ WaitGroup ============
fmt.Println("=== WaitGroup ===")
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 计数器 +1
go func(id int) {
defer wg.Done() // 计数器 -1
fmt.Printf("Goroutine %d 开始\n", id)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Goroutine %d 完成\n", id)
}(i)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("所有 goroutine 完成")
// ============ Mutex 互斥锁 ============
fmt.Println("\n=== Mutex ===")
var mu sync.Mutex
counter := 0
increment := func() {
mu.Lock()
defer mu.Unlock()
counter++
}
var wg2 sync.WaitGroup
for i := 0; i < 1000; i++ {
wg2.Add(1)
go func() {
defer wg2.Done()
increment()
}()
}
wg2.Wait()
fmt.Println("计数器:", counter) // 1000
// ============ RWMutex 读写锁 ============
fmt.Println("\n=== RWMutex ===")
var rwmu sync.RWMutex
data := make(map[string]int)
// 写操作用 Lock/Unlock
writeData := func(key string, value int) {
rwmu.Lock()
defer rwmu.Unlock()
data[key] = value
}
// 读操作用 RLock/RUnlock
readData := func(key string) int {
rwmu.RLock()
defer rwmu.RUnlock()
return data[key]
}
writeData("a", 1)
writeData("b", 2)
fmt.Println("读取 a:", readData("a"))
fmt.Println("读取 b:", readData("b"))
// ============ Once ============
fmt.Println("\n=== Once ===")
var once sync.Once
for i := 0; i < 5; i++ {
once.Do(func() {
fmt.Println("只执行一次")
})
}
// ============ Pool 对象池 ============
fmt.Println("\n=== Pool ===")
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("创建新对象")
return 0
},
}
// 从池中获取
obj1 := pool.Get().(int)
fmt.Println("获取:", obj1)
// 放回池中
pool.Put(42)
// 再次获取(可能获取到刚才放回的)
obj2 := pool.Get().(int)
fmt.Println("再次获取:", obj2)
}
生活场景:
goroutine就像"多个人同时干活"channel就像"管道",用来传递东西WaitGroup就像"点名册",等所有人到齐再开始Mutex就像"厕所锁",一次只能一个人用RWMutex就像"图书馆",多人可以同时读,但写的时候只能一个人
包管理
包的导入和使用
go
package main
import (
"fmt" // 标准库
"math" // 标准库
"os" // 标准库
"strings" // 标准库
// 第三方包
// "github.com/gin-gonic/gin"
// 别名
myfmt "fmt"
// 只执行初始化代码
_ "database/sql"
)
// 导出规则:首字母大写 = 公开,首字母小写 = 私有
// 公开函数(可以被其他包调用)
func Add(a, b int) int {
return a + b
}
// 私有函数(只能在当前包内使用)
func subtract(a, b int) int {
return a - b
}
func main() {
// 使用标准库
fmt.Println("Hello")
fmt.Println("圆周率:", math.Pi)
fmt.Println("平方根:", math.Sqrt(2))
// 使用别名
myfmt.Println("使用别名")
// 字符串操作
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.Split("a,b,c", ",")) // [a b c]
fmt.Println(strings.Contains("hello", "ell")) // true
// 命令行参数
fmt.Println("参数数量:", len(os.Args))
fmt.Println("参数:", os.Args)
}
go mod - 模块管理
bash
# 初始化模块
go mod init myproject
# 下载依赖
go mod tidy
# 查看依赖
go list -m all
# 更新依赖
go get -u
# 清理未使用的依赖
go mod tidy
实战案例
案例1:简单的 Web 服务器
go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// 处理根路径
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "欢迎来到我的网站!")
})
// 处理 /hello 路径
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "世界"
}
fmt.Fprintf(w, "你好,%s!", name)
})
// 处理 /api 路径
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"message": "这是 API 响应"}`)
})
// 启动服务器
fmt.Println("服务器启动在 http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
案例2:并发下载器
go
package main
import (
"fmt"
"io"
"net/http"
"os"
"sync"
"time"
)
func downloadFile(url, filename string, wg *sync.WaitGroup) error {
defer wg.Done()
start := time.Now()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
fmt.Printf("下载完成: %s (耗时: %v)\n", filename, time.Since(start))
return nil
}
func main() {
urls := []struct {
url string
filename string
}{
{"https://example.com/file1", "file1.txt"},
{"https://example.com/file2", "file2.txt"},
{"https://example.com/file3", "file3.txt"},
}
var wg sync.WaitGroup
for _, item := range urls {
wg.Add(1)
go downloadFile(item.url, item.filename, &wg)
}
wg.Wait()
fmt.Println("所有下载完成")
}
案例3:简单的任务队列
go
package main
import (
"fmt"
"time"
)
type Task struct {
ID int
Name string
}
func worker(id int, tasks <-chan Task, results chan<- string) {
for task := range tasks {
fmt.Printf("工作者 %d 开始处理任务 %d: %s\n", id, task.ID, task.Name)
time.Sleep(500 * time.Millisecond) // 模拟处理时间
results <- fmt.Sprintf("工作者 %d 完成任务 %d", id, task.ID)
}
}
func main() {
tasks := make(chan Task, 10)
results := make(chan string, 10)
// 启动 3 个工作者
for i := 1; i <= 3; i++ {
go worker(i, tasks, results)
}
// 添加任务
for i := 1; i <= 5; i++ {
tasks <- Task{
ID: i,
Name: fmt.Sprintf("任务-%d", i),
}
}
close(tasks)
// 收集结果
for i := 1; i <= 5; i++ {
fmt.Println(<-results)
}
fmt.Println("所有任务完成")
}
最佳实践
1. 命名规范
go
// ✅ 好的命名
func calculateTotalPrice(items []Item) float64 { ... }
type UserService struct { ... }
const MAX_RETRY_COUNT = 3
// ❌ 不好的命名
func calc(items []Item) float64 { ... } // 太短
type UserSvc struct { ... } // 缩写不清晰
const max = 3 // 太短
2. 错误处理
go
// ✅ 好的错误处理
func ReadConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("读取配置文件 %s 失败: %w", filename, err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
return &config, nil
}
// ❌ 不好的错误处理
func readConfig(filename string) *Config {
data, _ := os.ReadFile(filename) // 忽略错误
var config Config
json.Unmarshal(data, &config) // 忽略错误
return &config
}
3. 并发安全
go
// ✅ 线程安全
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) GetCount() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// ❌ 非线程安全
type UnsafeCounter struct {
count int
}
func (c *UnsafeCounter) Increment() {
c.count++ // 多个 goroutine 同时访问会出问题
}
4. 资源管理
go
// ✅ 使用 defer 确保资源释放
func ReadFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 确保文件会被关闭
// 处理文件...
return nil
}
// ✅ 使用 context 控制超时
func FetchData(ctx context.Context) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
5. 代码组织
go
// ✅ 好的项目结构
/*
myproject/
├── cmd/ # 主程序入口
│ └── myapp/
│ └── main.go
├── internal/ # 私有代码
│ ├── handler/
│ ├── service/
│ └── repository/
├── pkg/ # 公共代码
│ └── utils/
├── api/ # API 定义
├── configs/ # 配置文件
├── scripts/ # 脚本
├── go.mod
└── go.sum
*/
// ✅ 接口定义放在使用方
// 而不是实现方
package service
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
推荐资源
-
官方资源
-
书籍
- 《Go 语言圣经》
- 《Go 语言实战》
- 《Go 语言高级编程》
-
在线练习
- Go Playground
- LeetCode(用 Go 刷题)
- Exercism
常见陷阱
- 循环变量陷阱
go
// ❌ 错误:所有 goroutine 都会打印 3
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
// ✅ 正确:把 i 作为参数传入
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
- 切片引用陷阱
go
// ❌ 错误:切片引用同一底层数组
a := []int{1, 2, 3}
b := a[:2]
b[0] = 999 // a[0] 也变成 999
// ✅ 正确:使用 copy
a := []int{1, 2, 3}
b := make([]int, 2)
copy(b, a[:2])
b[0] = 999 // a 不受影响
- nil 接口陷阱
go
// ❌ 错误:nil 接口不等于 nil
var p *int // p 是 nil
var i interface{} = p
fmt.Println(i == nil) // false!因为 i 有类型信息
// ✅ 正确:检查 nil 接口
var i interface{}
fmt.Println(i == nil) // true
- defer 参数陷阱
go
// ❌ 错误:defer 使用最终值
i := 0
defer fmt.Println(i) // 打印 0
i = 10
// ✅ 正确:立即求值
i := 0
defer func(n int) {
fmt.Println(n) // 打印 0
}(i)
i = 10
// 或者
i := 0
defer func() {
fmt.Println(i) // 打印 10
}()
i = 10
总结
Go 语言的核心特点:
- 简单 - 语法简洁,只有 25 个关键字
- 高效 - 编译快,运行快
- 并发 - goroutine 和 channel 让并发变得简单
- 安全 - 强类型,编译时检查
- 实用 - 标准库丰富,工具链完善
记住这些核心概念:
- 变量 - 存储数据的盒子
- 函数 - 可重用的代码块
- 结构体 - 自定义数据类型
- 接口 - 行为的抽象
- goroutine - 轻量级线程
- channel - goroutine 之间的通信管道
小虾:青蛙掀门帘---露一小手!🦐 祝大家轻松、愉快、高效的学习!🦐