Go 变量、类型和流程控制:把基础语法写得清清楚楚

本文用真实业务片段讲解 Go 的变量声明、零值、常量、基本类型、if、for、switch 和作用域,帮助初学者把基础语法写扎实。

基础语法不是背规则,而是建立手感

很多人学 Go 的第一反应是:语法不难,但总觉得写起来有些规矩。变量声明有好几种方式,类型写在变量名后面,if 没有括号,循环只有 forswitch 默认不贯穿。单独看每条规则都不复杂,真正需要练的是判断:什么时候该显式写类型,什么时候可以让编译器推断,什么时候应该用 switch 让代码更清楚。

Go 的基础语法很强调可读性。它不追求用最短的字符表达最多的东西,而是希望一段代码交给同事时,对方不用猜你的意图。变量名、类型、作用域和控制流都应该服务于这个目标。

这篇文章会从变量和类型讲起,再讲 ifforswitch。示例尽量贴近真实后端代码,比如订单金额、用户状态、配置开关和分页参数。你不需要一次记住所有细节,但要开始形成 Go 的写法习惯。

变量声明的几种方式

Go 最完整的变量声明是:

var name string = "小林"

这行代码同时写出了变量名、类型和值。它当然正确,但在函数内部并不总是必要。因为右侧已经是字符串,编译器能推断类型,所以可以写成:

var name = "小林"

在函数内部,最常见的是短变量声明:

name := "小林"
age := 28
active := true

:= 只能用于函数内部,不能用于包级变量。它适合局部变量,因为局部上下文短,类型通常一眼能看出来。

包级变量一般更谨慎:

var serviceName = "user-api"
var maxRetryCount int = 3

包级状态会影响整个包,使用时要克制。初学阶段不要把所有东西都放到全局变量里。能作为参数传递的,就尽量作为参数传递。

零值:Go 很重视默认状态

Go 变量声明后即使没有赋值,也会有零值:

var name string
var count int
var enabled bool

fmt.Println(name)    // ""
fmt.Println(count)   // 0
fmt.Println(enabled) // false

零值让 Go 代码少了很多未初始化错误。你可以放心声明一个变量,再根据条件赋值。

func discountRate(level string) float64 {
	var rate float64

	if level == "gold" {
		rate = 0.20
	} else if level == "silver" {
		rate = 0.10
	}

	return rate
}

如果不是金牌或银牌用户,rate 保持 0。这段代码是安全的。但安全不代表一定清楚。很多时候直接返回会更明确:

func discountRate(level string) float64 {
	if level == "gold" {
		return 0.20
	}
	if level == "silver" {
		return 0.10
	}
	return 0
}

Go 的零值很有用,但不要用它隐藏业务意图。如果 0 表示“没有折扣”,可以接受。如果 0 表示“未知状态”,最好显式建模。

基本类型要选得自然

Go 常用基本类型包括:

string
bool
int
int64
float64
byte
rune

业务代码里,整数默认使用 int 就可以。需要和数据库、自增 ID、时间戳或外部协议对齐时,才更常见地使用 int64。金额不要随便用 float64 表示真实货币,因为浮点数有精度问题。入门示例可以用 float64 表示折扣率、评分、比例,但订单金额更建议用“分”为单位的整数:

type Order struct {
	ID          int64
	TotalCents int64
	Paid        bool
}

字符串是 string,但要注意中文字符不是一个字节。下面代码输出的长度不是 2:

name := "小林"
fmt.Println(len(name))

len 返回字节数。中文字符在 UTF-8 中通常占多个字节。如果你要按字符遍历,应使用 range

for _, r := range "小林" {
	fmt.Printf("%c\n", r)
}

这里的 r 类型是 rune,本质上是 Unicode 码点。初学者不必深入编码细节,但要记住:处理中文时,不要把字节长度当成字符数量。

常量适合表达稳定规则

常量用 const

const defaultPageSize = 20
const maxPageSize = 100

它适合表达不会在运行时改变的规则。比如分页默认数量、订单状态值、配置键名。

const (
	StatusPending = "pending"
	StatusPaid    = "paid"
	StatusClosed  = "closed"
)

Go 里常量没有必须大写的要求。首字母大写意味着导出,能被其他包访问;首字母小写表示包内使用。如果这些状态只在当前包里使用,写成小写也很好:

const (
	statusPending = "pending"
	statusPaid    = "paid"
	statusClosed  = "closed"
)

不要为了“像常量”就全部大写。Go 社区很少写 MAX_PAGE_SIZE 这种风格,除非你在对接已有协议或环境变量名。

if:让判断贴近业务语言

Go 的 if 没有括号:

if age >= 18 {
	fmt.Println("adult")
}

可以带初始化语句:

if discount := discountRate("gold"); discount > 0 {
	fmt.Printf("discount: %.0f%%\n", discount*100)
}

这里 discount 只在 if 和对应 else 范围内可见。这个写法适合临时变量,能减少作用域污染。

真实代码里,更常见的是提前返回:

func canCheckout(stock int, userActive bool) bool {
	if stock <= 0 {
		return false
	}
	if !userActive {
		return false
	}
	return true
}

这种写法比多层嵌套更容易读。Go 代码经常这样处理错误和边界条件:先把不能继续的情况排除掉,最后留下正常路径。

不太建议写成:

func canCheckout(stock int, userActive bool) bool {
	if stock > 0 {
		if userActive {
			return true
		}
	}
	return false
}

它能运行,但读者要在脑子里穿过两层条件。业务条件越多,嵌套越难维护。

for:Go 只有一种循环

Go 没有 while,循环统一使用 for

普通计数循环:

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

类似 while 的写法:

count := 0
for count < 5 {
	fmt.Println(count)
	count++
}

无限循环:

for {
	fmt.Println("working")
	break
}

遍历切片使用 range

names := []string{"小林", "阿周", "老陈"}

for index, name := range names {
	fmt.Printf("%d: %s\n", index, name)
}

如果不需要索引,用 _ 忽略:

for _, name := range names {
	fmt.Println(name)
}

遍历 map:

scores := map[string]int{
	"小林": 95,
	"阿周": 88,
}

for name, score := range scores {
	fmt.Printf("%s: %d\n", name, score)
}

要注意 map 遍历顺序是不固定的。你不能依赖它每次输出一样顺序。如果需要排序,先取出 key,排序后再访问。

switch:比一串 else if 更像规则表

当判断同一个变量的多个取值时,switch 很清楚:

func statusText(status string) string {
	switch status {
	case "pending":
		return "待支付"
	case "paid":
		return "已支付"
	case "closed":
		return "已关闭"
	default:
		return "未知状态"
	}
}

Go 的 switch 默认不会贯穿到下一个 case,所以不需要写 break。这减少了很多传统 C 风格 switch 的错误。

多个值可以放在同一个 case

func isFinalStatus(status string) bool {
	switch status {
	case "paid", "closed", "refunded":
		return true
	default:
		return false
	}
}

也可以不带表达式,把它当成更清楚的 if else

func shippingFee(totalCents int64) int64 {
	switch {
	case totalCents >= 19900:
		return 0
	case totalCents >= 9900:
		return 500
	default:
		return 1200
	}
}

这段代码比嵌套判断更像一张规则表。业务规则一多,switch 往往比连续 else if 更容易维护。

作用域:变量活得越短越好

Go 的作用域规则直接影响代码质量。变量应该尽量靠近使用它的地方声明。

func printOrder(orderID int64) {
	orderNo := fmt.Sprintf("NO-%06d", orderID)
	fmt.Println(orderNo)
}

不要把局部变量提前声明到很远的地方:

func printOrder(orderID int64) {
	var orderNo string

	// 中间隔了很多代码
	orderNo = fmt.Sprintf("NO-%06d", orderID)
	fmt.Println(orderNo)
}

变量作用域越大,读者需要记住的状态越多。Go 的短变量声明、if 初始化语句、for 初始化语句,都是为了让变量留在尽量小的范围内。

也要避免无意中遮蔽变量:

count := 10

if true {
	count := 20
	fmt.Println(count) // 20
}

fmt.Println(count) // 10

里面的 count := 20 创建了新变量,不是修改外面的 count。这种情况有时是故意的,有时是 bug。初学阶段遇到值没变,先检查是不是用了 :=

写一个分页参数清洗函数

把前面的知识组合起来,写一个常见函数:清洗分页参数。

package main

import "fmt"

const (
	defaultPage     = 1
	defaultPageSize = 20
	maxPageSize     = 100
)

func normalizePagination(page int, pageSize int) (int, int) {
	if page <= 0 {
		page = defaultPage
	}

	switch {
	case pageSize <= 0:
		pageSize = defaultPageSize
	case pageSize > maxPageSize:
		pageSize = maxPageSize
	}

	return page, pageSize
}

func main() {
	page, pageSize := normalizePagination(-1, 500)
	fmt.Printf("page=%d pageSize=%d\n", page, pageSize)
}

这段代码包含常量、变量、ifswitch 和多返回值。它不复杂,却很像真实后端服务里的参数处理。

为什么不直接在调用处到处写判断?因为分页规则是一件独立事情。把它封装成函数,调用者只需要相信结果已经合法:

page, pageSize := normalizePagination(req.Page, req.PageSize)

学习基础语法时,最好总是问一句:这段语法在真实代码里解决什么问题?当你能把语法和场景连起来,记忆会牢得多。

小结

Go 的变量、类型和流程控制都不难,但它们共同塑造了一种风格:显式、短作用域、少嵌套、规则清楚。变量声明不要炫技,能让读者看懂最重要;类型选择要贴近业务,尤其是金额、ID、状态这类字段;if 适合处理边界,switch 适合表达规则,for 统一处理所有循环。

写 Go 基础代码时,可以先记住几个简单判断:局部变量优先用 :=,包级变量少用;能提前返回就少嵌套;中文字符串不要用字节长度当字符数量;map 遍历不要依赖顺序;变量作用域越小越好。

这些习惯看起来细,但会一直伴随你。后面写函数、结构体、接口和并发代码时,基础语法的清晰度会直接决定程序是否容易维护。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页