第二章:Go语法进阶——变量、常量、数据类型与流程控制

好,咱们接着往下聊。上一章我们把Go的环境搭好了,也跑通了第一个Hello World。这一章,咱们得把地基打牢——变量、常量、数据类型、流程控制,这些是写任何Go程序都绕不开的东西。

说实话,Go的语法设计得很克制。它不像某些语言那样花里胡哨,但每个设计都有它的道理。我刚开始从Python转Go的时候,觉得这语言怎么这么啰嗦?用久了才发现,这种啰嗦恰恰是它的优点——代码可读性极强,团队协作时几乎不会产生歧义。

2.1 变量声明:Go的几种写法

Go的变量声明有好几种方式,咱们一个一个来看。

2.1.1 标准声明

最传统的写法,用var关键字。我个人习惯在包级别变量或者需要明确类型时用这种。

var name string = "张三"
var age int = 25
var isStudent bool = true

2.1.2 类型推断

如果声明的同时赋值,Go可以自动推断类型。省事多了。

var name = "张三"   // 自动推断为 string
var age = 25        // 自动推断为 int

2.1.3 短变量声明(最常用)

在函数内部,用:=是最爽的写法。我写业务代码时,90%的场景都用这个。

name := "张三"
age := 25
我的习惯:函数内部一律用:=,包级别变量用var。这样代码看起来层次分明,别人一看就知道变量的作用域。

2.1.4 多变量声明

一次声明多个变量,Go也支持得很优雅。

var x, y int = 1, 2
a, b := "hello", 42

我曾经在写一个配置解析器时,一口气声明了七八个变量,用这种多变量声明方式,代码看起来特别清爽。

2.2 常量:用const锁死它

常量就是不会变的值。Go里用const关键字声明。

const Pi = 3.14159
const MaxRetries = 3

常量可以是字符、字符串、布尔或数值类型。注意,常量不能用:=声明。

2.2.1 iota:枚举的利器

Go没有专门的枚举类型,但iota可以帮我们实现类似的效果。我第一次看到iota时觉得这玩意儿有点玄乎,用多了才发现真香。

const (
    StatusPending = iota  // 0
    StatusRunning         // 1
    StatusDone            // 2
    StatusFailed          // 3
)

iotaconst块中每行递增1。如果你跳过某行,它也会继续递增。

注意:iota只在const块中有效。我曾经在函数内部试图用iota,结果编译报错,愣是找了半天才反应过来。

2.3 基本数据类型

Go的数据类型不算多,但每个都很实在。咱们快速过一遍。

2.3.1 布尔型

bool,只有truefalse。没啥好说的,注意Go不允许把非布尔值当布尔用。

var isReady bool = true

2.3.2 数值型

整数类型分有符号和无符号,还有不同位数。

类型 范围 用途
int 32位或64位(取决于平台) 通用整数
int8 -128 ~ 127 小范围整数
int16 -32768 ~ 32767 中等范围
int32 -2^31 ~ 2^31-1 常用
int64 -2^63 ~ 2^63-1 大整数
uint 0 ~ 取决于平台 非负整数
uint8 0 ~ 255 字节
float32 约±3.4e38 单精度浮点
float64 约±1.8e308 双精度浮点(推荐)

我个人建议,整数默认用int,浮点默认用float64。除非你有明确的性能或内存约束,否则别瞎折腾。

2.3.3 字符串

Go的字符串是UTF-8编码的,不可变。用双引号或反引号。

s1 := "hello"
s2 := `多行
字符串`

字符串拼接用+,但大量拼接时建议用strings.Builder,性能更好。

2.3.4 零值

Go有个很贴心的设计:变量声明后如果没有赋值,会自动给一个零值。不会像C语言那样出现未初始化的问题。

var i int       // 0
var f float64   // 0.0
var s string    // ""
var b bool      // false
避坑指南:我曾经在写一个网络服务时,忘了初始化一个map变量,结果运行时直接panic。因为map的零值是nil,往nil map里写数据会崩溃。记住:mapslicechannel这些引用类型,用之前一定要make一下。

2.4 流程控制

Go的流程控制关键字不多,但每个都设计得很精炼。

2.4.1 if 语句

Go的if没有括号,但必须有花括号。这个设计我很喜欢,强迫你写出结构清晰的代码。

if score >= 90 {
    fmt.Println("优秀")
} else if score >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}

Go的if还支持一个特殊写法:在条件前加一个简单语句。

if err := doSomething(); err != nil {
    fmt.Println("出错了:", err)
}

这个写法在Go里非常常见,尤其是处理错误时。变量err的作用域只在if-else块内,不会污染外部命名空间。

2.4.2 for 循环

Go只有一个循环关键字——for。但它能玩出三种花样。

标准for循环:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

类似while的写法:

n := 0
for n < 10 {
    n++
}

无限循环:

for {
    // 一直跑,直到 break
}

我写后台服务时,经常用无限循环配合select做事件驱动。这个后面讲到并发时再细说。

2.4.3 switch 语句

Go的switch比C语言聪明多了。它默认每个case后面自动break,不会往下穿透。如果你想穿透,得显式用fallthrough

day := "周一"
switch day {
case "周一", "周二", "周三", "周四", "周五":
    fmt.Println("工作日")
case "周六", "周日":
    fmt.Println("周末")
default:
    fmt.Println("未知")
}

Go的switch还可以不带表达式,直接当if-else链用。

score := 85
switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
default:
    fmt.Println("C")
}

这种写法在条件分支较多时,比if-else链更清晰。我个人很喜欢用。

2.5 函数与多返回值

函数是Go的核心。Go的函数有几个特点:支持多返回值、支持命名返回值、函数是一等公民。

2.5.1 基本函数定义

func add(a int, b int) int {
    return a + b
}

如果参数类型相同,可以简写:

func add(a, b int) int {
    return a + b
}

2.5.2 多返回值

这是Go最实用的特性之一。最常见的用法是返回结果和错误。

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil
}

调用时:

result, err := divide(10, 2)
if err != nil {
    fmt.Println("出错了:", err)
} else {
    fmt.Println("结果:", result)
}

这种模式在Go里无处不在。我刚开始写Go时觉得每次都要检查err很烦,但写久了发现,这种显式的错误处理反而让代码更健壮。你不会忘记处理错误,因为编译器会提醒你。

2.5.3 命名返回值

Go允许给返回值起名字。这样在函数体内可以直接给这些变量赋值,最后用return裸返回。

func getRectangle(width, height float64) (area, perimeter float64) {
    area = width * height
    perimeter = 2 * (width + height)
    return
}

命名返回值在复杂函数中很有用,但别滥用。我建议只在返回值含义明确、函数逻辑清晰时使用。

2.5.4 可变参数

如果你不确定参数个数,可以用...

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// 调用
fmt.Println(sum(1, 2, 3))     // 6
fmt.Println(sum(1, 2, 3, 4))  // 10

2.5.5 函数作为值

在Go里,函数也是一种类型。你可以把函数赋值给变量,或者作为参数传递。

// 函数赋值给变量
add := func(a, b int) int {
    return a + b
}
fmt.Println(add(3, 4))

// 函数作为参数
func applyOperation(a, b int, op func(int, int) int) int {
    return op(a, b)
}
fmt.Println(applyOperation(5, 3, add))

这个特性在写回调函数、中间件时特别有用。后面讲HTTP服务时,你会看到大量这种用法。

我的建议:多返回值是Go的精华,一定要用好。但别滥用——如果返回超过3个值,建议用结构体封装一下,否则调用方会搞不清顺序。

2.6 小结

这一章的内容不少,但都是基本功。变量声明、常量、数据类型、流程控制、函数——这些是写任何Go程序都离不开的。

我个人觉得,Go的语法设计很务实。它没有太多花哨的语法糖,但每个特性都解决了一个实际问题。比如多返回值解决了错误处理的痛点,短变量声明让代码更简洁,iota让枚举更优雅。

下一章,咱们要进入Go的核心领域——复合类型。数组、切片、映射、结构体,这些才是真正让你写出复杂程序的基础。准备好了吗?