跳到主要内容
预计阅读 25 分钟

基础语法:变量、类型、控制流与函数 — Go语言的”基本功”

学武功要先扎马步,学编程要先打语法基础。Go的语法就像一套简化太极——招式不多,但每一招都干净利落。

📋 开篇自测:你已经知道多少?

  1. Go的变量声明有几种方式?:=var 有什么区别?
  2. Go的 switch 语句和C/Java的有什么本质区别?
  3. 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 整数类型

类型大小范围
int81字节-128 到 127
int162字节-32768 到 32767
int324字节约 ±21亿
int648字节非常大
int平台相关32位或64位系统对应

还有无符号版本:uint8uint16uint32uint64uint

日常开发中,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中,你享受指针的便利,却不用担心指针的大部分陷阱。

🤔 想一想 什么时候应该传指针,什么时候应该传值?提示:考虑数据大小和是否需要修改原始数据。


📝 掌握度自测

  1. 以下哪种变量声明方式只能在函数内部使用?

    • A) var x int = 10
    • B) var x = 10
    • C) x := 10
    • D) const x = 10
  2. len("Go语言") 的结果是什么?

    • A) 4
    • B) 8
    • C) 6
    • D) 3
  3. Go中以下哪个循环关键字不存在?

    • A) for
    • B) while
    • C) range
    • D) break
  4. 关于defer,以下说法正确的是:

    • A) defer语句会立刻执行
    • B) 多个defer按先进先出顺序执行
    • C) 多个defer按后进先出顺序执行
    • D) defer只能用于关闭文件
  5. 以下代码的输出是什么?

    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