基础语法:变量、类型、控制流与函数 — Go语言的”基本功”
学武功要先扎马步,学编程要先打语法基础。Go的语法就像一套简化太极——招式不多,但每一招都干净利落。
📋 开篇自测:你已经知道多少?
- Go的变量声明有几种方式?
:=和var有什么区别?- Go的
switch语句和C/Java的有什么本质区别?- Go的函数可以返回多个值,这在实际开发中有什么好处?
一、变量与常量:给数据贴标签
编程的本质就是操作数据,而变量就是数据的”标签”。想象你搬家时给纸箱贴标签——“厨房用品”、“书籍”、“衣服”——变量就是告诉计算机”这块内存里放的是什么”。
1.1 变量声明的三种姿势
Go提供了多种声明变量的方式,每种都有适用场景。
姿势一:完整声明(var + 类型)
var name string = "张三"
var age int = 28
var salary float64 = 15000.50
这是最”规矩”的写法,适合需要明确类型的场景。
姿势二:类型推断(省略类型)
var name = "张三" // 编译器自动推断为 string
var age = 28 // 编译器自动推断为 int
var salary = 15000.50 // 编译器自动推断为 float64
Go的编译器很聪明,能从右边的值推断出左边变量的类型。
姿势三:短变量声明(:= 大法)
name := "张三"
age := 28
salary := 15000.50
这是Go程序员日常最爱用的方式——简洁到极致。但要注意::= 只能在函数内部使用,不能用于包级别的变量声明。
这三种方式的关系就像点咖啡:
- 完整声明 = “请给我一杯中杯、热的、拿铁咖啡”
- 类型推断 = “请给我一杯热拿铁”(杯型看你倒多少)
- 短声明 = “老样子”(老顾客专属,只在熟悉的环境用)
1.2 批量声明
当你需要同时声明多个变量时,可以用括号把它们包起来:
var (
name string = "李四"
age int = 30
active bool = true
)
1.3 零值机制
Go有一个非常友好的设计:所有变量在声明后都会自动初始化为”零值”,不会出现未初始化的垃圾数据。
var i int // 零值:0
var f float64 // 零值:0.0
var s string // 零值:""(空字符串)
var b bool // 零值:false
var p *int // 零值:nil
这就像新买的笔记本——每一页都是干净的白纸,而不是前任使用者留下的涂鸦。
1.4 常量
常量用 const 声明,一旦赋值就不能修改:
const Pi = 3.14159265358979
const AppName = "我的Go应用"
const MaxRetry = 3
Go还有一个独特的常量生成器 iota,特别适合定义枚举:
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
iota 就像一个自动计数器,每新增一行就自动加1。你还可以玩出花样:
const (
_ = iota // 跳过0
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1048576
GB // 1 << 30
TB // 1 << 40
)
🤔 想一想 为什么Go要设计零值机制?如果没有零值,你在写代码时需要额外做什么?
二、基础数据类型:Go的”元素周期表”
Go的类型系统简洁但完整,就像化学的元素周期表——基本元素不多,但能组合出万物。
2.1 整数类型
| 类型 | 大小 | 范围 |
|---|---|---|
int8 | 1字节 | -128 到 127 |
int16 | 2字节 | -32768 到 32767 |
int32 | 4字节 | 约 ±21亿 |
int64 | 8字节 | 非常大 |
int | 平台相关 | 32位或64位系统对应 |
还有无符号版本:uint8、uint16、uint32、uint64、uint。
日常开发中,用 int 就够了,除非你明确需要控制内存大小(比如处理网络协议或文件格式)。
2.2 浮点数类型
var price float64 = 99.99 // 64位浮点,精度更高,默认选择
var ratio float32 = 0.618 // 32位浮点,节省空间
一个重要的忠告:永远不要用浮点数比较是否相等。
// 注意:直接用字面量比较,Go会在编译期用任意精度常量运算,结果为true
fmt.Println(0.1 + 0.2 == 0.3) // true(常量表达式,编译器精确计算)
// 但一旦赋值给float64变量,就会出现精度丢失
a := 0.1
b := 0.2
fmt.Println(a + b == 0.3) // false!float64运算有精度误差
// 正确做法:判断差值是否足够小
diff := math.Abs(a + b - 0.3)
fmt.Println(diff < 1e-9) // true
这里有一个Go的特殊之处:Go的无类型常量使用任意精度算术,所以字面量 0.1 + 0.2 == 0.3 在编译期是精确相等的。但只要值落入 float64 变量,就会遵循IEEE 754浮点规则,产生精度误差。这就像用尺子量东西——标注3厘米的位置和实际3厘米之间总有微小误差,但只要误差小到可以忽略就行。
2.3 字符串
Go的字符串是不可变的UTF-8编码字节序列:
greeting := "你好,世界"
fmt.Println(len(greeting)) // 15(字节数,每个中文3字节)
fmt.Println(utf8.RuneCountInString(greeting)) // 5(字符数)
字符串操作常用技巧:
// 字符串拼接
full := "Hello" + " " + "Go"
// 多行字符串用反引号
query := `
SELECT *
FROM users
WHERE age > 18
`
// 格式化字符串
info := fmt.Sprintf("姓名:%s,年龄:%d", "王五", 25)
2.4 布尔类型
var isReady bool = true
var isEmpty bool = false
Go的布尔类型不能和整数互转——if 1 在Go中是语法错误。Go认为代码应该表达明确的意图,而不是依赖隐式转换。
2.5 类型转换
Go不支持隐式类型转换,所有转换都必须显式进行:
var i int = 42
var f float64 = float64(i) // int -> float64
var u uint = uint(f) // float64 -> uint
// 字符串和数字之间的转换需要 strconv 包
import "strconv"
s := strconv.Itoa(42) // int -> string: "42"
n, err := strconv.Atoi("42") // string -> int: 42
⚠️ 常见误区
- 误区一:认为
len("你好")返回2。实际返回6,因为len返回的是字节数,常用中文字符在UTF-8中占3个字节(极少数补充区汉字占4个字节)。- 误区二:试图用
(int)(3.14)进行类型转换。Go的语法是int(3.14),不需要外面的括号。- 误区三:认为Go有
char类型。Go使用byte(即uint8)和rune(即int32)来处理单个字符。
三、控制流:程序的”交通规则”
如果变量是”路上的车辆”,那控制流就是”交通信号灯和路标”——它决定了程序该走哪条路。
3.1 if-else:条件判断
Go的 if 语句有一个小特点——条件表达式不需要括号:
score := 85
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
Go还支持在 if 中先执行一个语句,再做判断——这个特性非常实用:
// 先打开文件,再判断是否出错
if file, err := os.Open("data.txt"); err != nil {
fmt.Println("打开失败:", err)
} else {
defer file.Close()
// 处理文件...
}
这就像”先尝一口再决定要不要吃”——把动作和判断写在一起,代码更紧凑。
3.2 for:Go唯一的循环
Go只有 for 一种循环——没有 while,没有 do-while。但别担心,for 足够灵活,一个人就能演三个角色。
经典三段式:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
充当while:
count := 0
for count < 5 {
fmt.Println(count)
count++
}
无限循环:
for {
// 这是一个死循环,通常配合 break 使用
input := readUserInput()
if input == "quit" {
break
}
}
遍历集合(for-range):
fruits := []string{"苹果", "香蕉", "橘子"}
for index, fruit := range fruits {
fmt.Printf("第%d个水果:%s\n", index, fruit)
}
// 如果不需要索引,用 _ 忽略
for _, fruit := range fruits {
fmt.Println(fruit)
}
3.3 switch:优雅的多路分支
Go的 switch 比C/Java优雅得多——每个case自动break,不会发生”穿透”问题:
day := "周三"
switch day {
case "周一":
fmt.Println("新的一周开始了")
case "周三":
fmt.Println("已经到周中了")
case "周五":
fmt.Println("快到周末了!")
case "周六", "周日":
fmt.Println("周末愉快!")
default:
fmt.Println("普通的一天")
}
如果你确实需要穿透到下一个case,用 fallthrough 关键字:
switch num := 5; {
case num > 0:
fmt.Println("正数")
fallthrough
case num > -10:
fmt.Println("大于-10")
}
Go的 switch 还能不写条件,变成一串 if-else 的替代品:
temperature := 35
switch {
case temperature >= 40:
fmt.Println("极端高温")
case temperature >= 35:
fmt.Println("高温预警")
case temperature >= 25:
fmt.Println("温暖舒适")
default:
fmt.Println("有点凉")
}
🤔 想一想 Go为什么要把switch的默认行为从”穿透”改为”自动break”?这体现了什么设计理念?
四、函数:代码的”乐高积木”
函数是程序的基本组织单元,就像乐高积木——每块积木(函数)完成一个小功能,组合起来就能搭出城堡(完整程序)。
4.1 函数定义
func greet(name string) string {
return "你好," + name + "!"
}
// 调用
message := greet("Go")
fmt.Println(message) // 你好,Go!
函数签名的格式是:func 函数名(参数列表) 返回类型。
4.2 多返回值
Go最让其他语言程序员眼前一亮的特性之一就是原生支持多返回值:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}
// 调用时接收两个返回值
result, err := divide(10, 3)
if err != nil {
fmt.Println("出错了:", err)
} else {
fmt.Printf("结果:%.2f\n", result)
}
多返回值最经典的用法就是返回 (结果, 错误)——这是Go处理错误的标准模式。不用try-catch,不用异常,一切都明明白白写在返回值里。
4.3 命名返回值
你可以给返回值起名字,这样在函数体内就能直接使用,最后用裸 return 返回:
func rectangleInfo(width, height float64) (area, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // 自动返回 area 和 perimeter
}
命名返回值就像给快递包裹贴好标签——打包的时候不用再一个个确认,直接发出去就行。不过在复杂函数中,建议还是显式 return area, perimeter,可读性更好。
4.4 可变参数
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
...int 表示接收任意数量的int参数,在函数内部它就是一个 []int 切片。
4.5 函数是一等公民
在Go中,函数可以赋值给变量、作为参数传递、作为返回值——函数就是一种类型:
// 函数赋值给变量
add := func(a, b int) int {
return a + b
}
fmt.Println(add(3, 4)) // 7
// 函数作为参数
func apply(f func(int, int) int, a, b int) int {
return f(a, b)
}
result := apply(add, 10, 20) // 30
4.6 defer:延迟执行
defer 是Go的一个精妙设计——被defer的函数调用会在当前函数返回之前执行:
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close() // 无论函数怎么返回,文件都会被关闭
// 读取文件内容...
}
defer 就像你出门前在门口放一个”记得锁门”的提示牌——不管你出门前做了什么,最后一步一定是锁门。
多个defer按**后进先出(LIFO)**的顺序执行:
func demo() {
defer fmt.Println("第一个defer")
defer fmt.Println("第二个defer")
defer fmt.Println("第三个defer")
}
// 输出顺序:第三个defer -> 第二个defer -> 第一个defer
这就像叠盘子——最后放上去的最先拿下来。
⚠️ 常见误区
- 误区一:认为defer会立刻执行。defer只是注册了一个延迟调用,等函数返回时才执行。
- 误区二:在循环中大量使用defer。defer的资源要等函数退出才释放,如果循环次数很多,可能导致资源耗尽。
- 误区三:忘记接收多返回值中的error。在Go中忽略错误是大忌,至少用
_显式丢弃:result, _ := divide(10, 0)。
五、指针:地址的力量
如果你有C语言背景,Go的指针会让你觉得很亲切但更安全。如果你来自Java或Python,别紧张——Go的指针比C简单得多。
5.1 什么是指针
指针就是一个变量的内存地址。打个比方:变量是你家的房子,指针是你家的门牌号。有了门牌号,快递员就能找到你家,不需要把整栋房子搬过去。
x := 42
p := &x // p 是指向 x 的指针,& 是取地址符
fmt.Println(p) // 输出一个内存地址,如 0xc0000b6010
fmt.Println(*p) // 输出 42,* 是解引用符,获取指针指向的值
*p = 100 // 通过指针修改 x 的值
fmt.Println(x) // 100
5.2 指针的实际用途
指针最常见的用途是避免数据复制和让函数能修改外部变量:
// 不用指针:函数拿到的是副本,修改不影响原值
func doubleValue(n int) {
n = n * 2 // 只修改了副本
}
// 用指针:函数拿到的是地址,可以修改原值
func doublePointer(n *int) {
*n = *n * 2 // 修改了原始变量
}
num := 10
doubleValue(num)
fmt.Println(num) // 还是 10
doublePointer(&num)
fmt.Println(num) // 变成 20
5.3 Go指针的安全性
Go的指针比C安全得多:
- 没有指针运算:不能对指针做加减操作
- 有垃圾回收:不用手动释放内存
- 不能指向随意的内存地址:只能指向合法的变量
所以在Go中,你享受指针的便利,却不用担心指针的大部分陷阱。
🤔 想一想 什么时候应该传指针,什么时候应该传值?提示:考虑数据大小和是否需要修改原始数据。
📝 掌握度自测
-
以下哪种变量声明方式只能在函数内部使用?
- A)
var x int = 10 - B)
var x = 10 - C)
x := 10 - D)
const x = 10
- A)
-
len("Go语言")的结果是什么?- A) 4
- B) 8
- C) 6
- D) 3
-
Go中以下哪个循环关键字不存在?
- A)
for - B)
while - C)
range - D)
break
- A)
-
关于defer,以下说法正确的是:
- A) defer语句会立刻执行
- B) 多个defer按先进先出顺序执行
- C) 多个defer按后进先出顺序执行
- D) defer只能用于关闭文件
-
以下代码的输出是什么?
x := 5 p := &x *p = 20 fmt.Println(x)- A) 5
- B) 20
- C) 内存地址
- D) 编译错误
💡 自我评估
- 答对5题:基础语法已经扎实,准备好迎接复合类型了!
- 答对3-4题:再回顾一下不确定的知识点,特别是指针和defer的部分。
- 答对0-2题:建议打开编辑器,把每个示例都亲手敲一遍,代码是练出来的。
参考答案: 1-C, 2-B(“Go”占2字节,“语”和”言”各占3字节,共2+3+3=8字节), 3-B, 4-C, 5-B
购买课程解锁全部内容
高并发不踩坑:Go 语言从语法到微服务
¥29.90