Go 基础语法
变量、常量、基本数据类型、零值、类型转换、iota、内建函数 min/max/clear、字符串操作等 Go 语言基础速查(基线 Go 1.27)。
1. 关键字与标识符
25 个关键字
| 类别 | 关键字 |
|---|---|
| 声明 | var const type func package import |
| 复合类型 | struct interface map chan |
| 控制流 | if else for switch case default select |
| 跳转 | break continue fallthrough goto return |
| 并发 | go defer |
| 其他 | range |
预定义标识符(非关键字,可被覆盖但不推荐)
| 类别 | 标识符 |
|---|---|
| 基本类型 | bool byte string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64 complex128 |
| 常量 | true false iota nil |
| 内置函数 | make len cap new append copy close delete panic recover print println real imag complex min max clear |
注意:
minmaxclear是 Go 1.21 加入的内置函数,模块go指令低于 1.21 时编译报built-in min requires go1.21 or later(实测),详见第 3 节。
2. 变量
var 声明
var name string = "Go" // 完整形式:var 名称 类型 = 值
var age int // 声明但未初始化,等于零值(0)
var name, age = "Go", 14 // 多变量声明 + 类型推断
var ( // 批量声明块
name string = "Go"
version int = 23
ok bool = true
)短变量声明 :=(函数内专用)
// 只能在函数内使用!
name := "Go" // 类型推断为 string
age := 14 // 类型推断为 int
name, ok := getValue() // 部分变量已存在时,仅声明新变量注意:
:=不能用于包级作用域。包级变量必须使用var关键字声明。
new 函数(Go 1.26 增强)
// Go 1.25 及之前:new(T) 仅接受类型,返回 *T(零值)
p := new(int) // *int,指向 0
user := new(User) // *User,所有字段零值
// Go 1.26+:new 支持带初始值的表达式,new(T) 旧用法不变
p := new(int64(300)) // *int64,指向 300(一行创建带值指针,实测)
u := new(User{Name: "Alice"}) // *User,复合字面量表达式同样合法(实测)
// 实用场景:JSON 可选字段
type Config struct {
Timeout *int64 `json:"timeout,omitempty"`
}
cfg := Config{Timeout: new(int64(30))} // Go 1.26:简洁
// 之前需要:t := int64(30); cfg := Config{Timeout: &t}注意:
new(expr)受语言版本门控——go.mod 的go指令低于 1.26 时编译报new(int64(300)) requires go1.26 or later(go1.27 实测)。建议场景:所有需要"分配并取地址带初值"的地方,尤其omitempty指针字段的单行初始化。
变量作用域 vs 短声明复用
func demo() {
x := 10
if true {
x := 20 // 新的局部 x,外部 x 不变(:= 总是声明新变量)
fmt.Println(x) // 20
}
fmt.Println(x) // 10
// vs 赋值(复用同一变量)
y := 10
if true {
y = 20 // 修改外部 y
}
fmt.Println(y) // 20
// := 部分复用
f, err := os.Open("a.txt") // f 和 err 都是新的
f2, err := os.Open("b.txt") // err 复用,f2 是新的(err 在同一作用域)
}类型推断
a := 42 // int(默认整数类型)
b := 3.14 // float64(默认浮点类型)
c := "hello" // string
d := 1 + 2i // complex128
e := 'A' // rune(int32 别名)3. 内置函数 min / max / clear(Go 1.21)
Go 1.21 把三个高频操作从"手写循环 / 删键循环"升级为语言级内建函数。类比 Python 的 min/max,Go 版支持任意多个同类型有序参数;clear 则统一了"清空 map"和"切片全部置零"两件事。
package main
import (
"fmt"
"math"
)
func main() {
// min / max:两个及以上参数,类型必须是同一有序类型(数值、字符串)
fmt.Println(min(3, 1, 2), max(3, 1, 2)) // 1 3
fmt.Println(min("apple", "banana")) // apple(字符串按字典序)
// 无类型常量参数会自动适配目标类型
x := min(3, 1.5)
fmt.Printf("%v (%T)\n", x, x) // 1.5 (float64)
// clear:map 删除所有键;切片所有元素置零值
m := map[string]int{"a": 1, "b": 2}
clear(m)
fmt.Println(len(m), m == nil) // 0 false(map 仍在,只是空了)
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s, len(s), cap(s)) // [0 0 0 0] 4 4(长度容量不变)
// NaN 特殊行为:只要参与比较,min/max 都返回 NaN
nan := math.NaN()
fmt.Println(min(nan, 1.0), max(nan, 1.0)) // NaN NaN
}以上输出为 go1.27.0 实测结果。
边界与规则
- 类型要求:参数只能是同一有序类型。无类型常量可以适配(
min(1, int64(2))合法);两个不同类型的变量混用直接编译错误mismatched types int (previous argument) and int64 (type of j)(实测)。 - NaN 陷阱:浮点参数里只要有一个 NaN,
min和max都返回 NaN(实测),不能用来"兜底取安全值"。 - clear 的参数范围:只接受 map 或 slice(含类型参数被约束为 map/slice 的情形)。数组不行——
clear一个[3]int编译报argument must be (or constrained by) map or slice(go1.27 实测)。数组置零请用var arr [3]int重新赋值整体。 - clear 对 nil:
clearnil map 或 nil slice 是安全的空操作,不 panic(实测)。 - clear 语义差别:map 是"删除所有键",底层桶可复用;slice 是"逐元素写零值",若元素是指针/结构体,这一步会真实释放引用,比
s = s[:0]更利于 GC。 - 版本门控:go.mod
go指令低于 1.21 时报built-in min requires go1.21 or later/clear requires go1.21 or later(实测)。
生产里 min/max 最常见于钳制配置值、计算超时上限;clear 常见于对象池回收、复用 map 避免重复分配。
4. 常量与 iota
常量声明
const Pi = 3.14159 // 无类型常量(更多灵活性)
const MaxSize int64 = 1 << 32 // 有类型常量
const ( // 批量声明
StatusOK = 200
StatusNotFound = 404
)
// iota:递增枚举器
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)iota 进阶用法
// 位掩码
const (
FlagRead = 1 << iota // 1 (1 << 0)
FlagWrite // 2 (1 << 1)
FlagExec // 4 (1 << 2)
)
// 跳值
const (
_ = iota // 0 跳过
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1048576
GB // 1 << 30
)
// 多表达式每行
const (
X, Y = iota, iota * 2 // 0, 0
A, B // 1, 2
C, D // 2, 4
)最佳实践:使用
iota替代硬编码的魔法数字。对于枚举类型的常量,用iota+ 有类型常量,可获取编译期类型安全。
无类型常量的精度
const huge = 1 << 100 // 无类型整数常量,精度远超 int64
fmt.Println(huge / (1 << 99)) // 2 — 编译期计算,不溢出
var x int64 = 1 << 62 // OK
var y int64 = 1 << 63 // 溢出 → 编译错误数字字面量:进制前缀与下划线分隔
自 Go 1.13 起,整数字面量支持二进制 0b、八进制 0o 前缀,以及任意位置的 _ 分隔符(纯粹为人眼服务,编译器忽略):
b := 0b1010 // 二进制 → 10
o := 0o777 // 八进制 → 511
h := 0xFF // 十六进制 → 255
n := 1_000_000 // 下划线分隔 → 1000000
m := 0xDE_AD_BE_EF // 十六进制里同样可用 → 3735928559
f := 6.02e23 // 浮点科学计数(Println 输出 6.02e+23)以上数值均为 go1.27 实测。老式八进制 0777(无前缀)仍然合法,但可读性差,新代码统一用 0o。分隔符不能贴着小数点或字母前缀(1._5、0_b1 非法)。
注意:下划线字面量受语言门控——go 指令为 1.12 及以下时报
underscore in numeric literal requires go1.13 or later(实测)。
5. 基本数据类型
类型速查
| 类型 | 大小 | 范围 | 零值 |
|---|---|---|---|
bool |
1 byte | true / false |
false |
int8 |
1 byte | -128 ~ 127 | 0 |
int16 |
2 bytes | -32768 ~ 32767 | 0 |
int32 / rune |
4 bytes | -2^31 ~ 2^31-1 | 0 |
int64 |
8 bytes | -2^63 ~ 2^63-1 | 0 |
uint8 / byte |
1 byte | 0 ~ 255 | 0 |
uint16 |
2 bytes | 0 ~ 65535 | 0 |
uint32 |
4 bytes | 0 ~ 2^32-1 | 0 |
uint64 |
8 bytes | 0 ~ 2^64-1 | 0 |
int / uint |
4 或 8 bytes | 取决于平台 | 0 |
float32 |
4 bytes | IEEE 754 | 0.0 |
float64 |
8 bytes | IEEE 754 | 0.0 |
complex64 |
8 bytes | 实部+虚部各 float32 | 0+0i |
complex128 |
16 bytes | 实部+虚部各 float64 | 0+0i |
string |
16 bytes (头) | UTF-8 | "" |
uintptr |
4 或 8 bytes | 指针大小的无符号整数 | 0 |
选择指南:整数默认用
int;明确大小要求用int64/int32;字节用byte(uint8);Unicode 码点用rune(int32)。浮点默认float64。
类型别名 vs 类型定义
// 类型别名(Go 1.9+):完全等同原类型
type MyInt = int // MyInt 和 int 完全相同
var a MyInt = 10
var b int = a // OK
// 类型定义:创建新类型
type MyInt int // MyInt 是基于 int 的新类型
var a MyInt = 10
var b int = a // 编译错误!需要显式转换
var b int = int(a) // OK深入原理:Go 1.9 引入类型别名是为了渐进式代码迁移——允许将类型从一个包移到另一个包而不破坏调用方。
rune和byte本身就是类型别名的典型用例(rune = int32,byte = uint8)。
6. 零值
Go 中所有变量声明后都有确定的值,没有 null/undefined/uninitialized:
var i int // 0
var f float64 // 0.0
var s string // ""
var b bool // false
var p *int // nil
var sl []int // nil
var m map[int]int // nil
var ch chan int // nil
var fn func() // nil
var in interface{} // nil最佳实践:零值机制让你能在声明后立即使用变量而不必显式初始化。善用零值语义(如用
int零值=0 做计数器),减少样板代码。结构体的零值是所有字段都为零值。
7. 类型转换
Go 没有隐式类型转换,所有转换都必须显式:
var i int = 42
var f float64 = float64(i) // 显式转换
var u uint = uint(f)
// 数值与字符串互转
import "strconv"
s := strconv.Itoa(42) // int → string ("42")
n, err := strconv.Atoi("42") // string → int (42, nil)
f, err := strconv.ParseFloat("3.14", 64) // string → float64
b, err := strconv.ParseBool("true") // string → bool字符串与 []byte / []rune
s := "hello"
bs := []byte(s) // string → []byte
rs := []rune(s) // string → []rune(按 Unicode 拆分)
s2 := string(bs) // []byte → string
s3 := string(rs) // []rune → string注意:
string([]byte)转换会复制底层数据(Go 中 string 不可变),频繁转换会有性能开销。在热路径中,有时用unsafe.Pointer避免复制——但必须确保字节切片不会被修改。
8. 字符串
解释字符串 vs 原始字符串
Go 有两种字符串字面量,差别在于是否处理转义序列:
esc := "line1\nline2\t中文" // 解释字符串:\n \t 等转义生效
raw := `line1\nline2` // 原始字符串(反引号):所见即所得,\n 就是两个字符
fmt.Println(len(esc)) // 18(\n、\t 各占 1 字节,中文各 3 字节,实测)
fmt.Println(len(raw)) // 12(反斜杠+n 本身被计为 2 个字符,实测)原始字符串内部不能出现反引号本身,但可以直接换行、不需要对 \ 做任何转义——写正则、嵌入 JSON 模板、内联脚本时首选,省掉成对的 \\ 噪音:
re := `^\d{3}-\d{4}$` // 不用写成 "^\\d{3}-\\d{4}$"byte 与 rune:两种"字符"视角
Go 字符串底层是 UTF-8 字节序列。索引拿到的是字节(byte/uint8),一个非 ASCII 字符会占 1~4 个字节;想要 Unicode 码点,用 rune(int32 别名):
s := "Go语言"
b := s[0] // byte(uint8) 'G'
r := []rune(s)[2] // rune(int32) '语' ——按码点定位,前两个 ASCII 各占 1 个码点
fmt.Printf("%T %c / %T %c\n", b, b, r, r) // uint8 G / int32 语(实测)for i, r := range s 会自动按 UTF-8 解码,i 是字节下标、r 是 rune——这是遍历"字符"的正确姿势(详见第 10 节陷阱)。
基本操作
s := "Hello, 世界" // Go 源码和字符串都是 UTF-8
// 长度(字节数,不是字符数!)
len(s) // 13(每个中文字符 3 字节)
// 字符数
import "unicode/utf8"
utf8.RuneCountInString(s) // 9
// 遍历字符(不是字节)
for i, r := range s {
fmt.Printf("%d: %c\n", i, r) // r 是 rune 类型
}
// 索引(按字节)
s[0] // 'H' (byte)
s[7] // 字节,不完整的中文字符!不要这么做
// 字符串不可变
s[0] = 'h' // 编译错误!cannot assign to s[0]字符串拼接
s1 := "hello"
s2 := "world"
// 方式一:+ 运算符(少量拼接用)
s := s1 + ", " + s2
// 方式二:fmt.Sprintf(需要格式化时)
s := fmt.Sprintf("%s, %s", s1, s2)
// 方式三:strings.Builder(大量拼接,性能最优)
var builder strings.Builder
builder.WriteString(s1)
builder.WriteString(", ")
builder.WriteString(s2)
s := builder.String()
// 方式四:strings.Join(拼接切片)
s := strings.Join([]string{s1, s2}, ", ")性能提示:循环中大量拼接字符串时,务必使用
strings.Builder(Go 1.10+)或bytes.Buffer。直接+拼接会产生大量临时字符串和内存分配。strings.Builder内部使用unsafe.Pointer避免字节复制。
常用 strings 函数
| 函数 | 说明 | 示例 |
|---|---|---|
strings.Contains(s, sub) |
是否包含子串 | Contains("abc", "b") → true |
strings.HasPrefix(s, pre) |
是否有前缀 | HasPrefix("abc", "a") → true |
strings.HasSuffix(s, suf) |
是否有后缀 | HasSuffix("abc", "c") → true |
strings.Index(s, sub) |
首次出现位置 | Index("abc", "b") → 1 |
strings.Cut(s, sep) |
按首个 sep 切成两段 | Cut("a.b.c", ".") → "a", "b.c", true(实测) |
strings.CutLast(s, sep) |
按最后 sep 切成两段(Go 1.27) | CutLast("a.b.c", ".") → "a.b", "c", true(实测) |
strings.Replace(s, old, new, n) |
替换 n 次(-1=全部) | Replace("aaa", "a", "b", 2) → "bba" |
strings.ToLower(s) |
转小写 | ToLower("ABC") → "abc" |
strings.ToUpper(s) |
转大写 | ToUpper("abc") → "ABC" |
strings.Trim(s, cut) |
去头尾字符 | Trim(" abc ", " ") → "abc" |
strings.Split(s, sep) |
分割 | Split("a,b", ",") → ["a", "b"] |
strings.Join(parts, sep) |
拼接 | Join(["a","b"], ",") → "a,b" |
strings.Repeat(s, n) |
重复 n 次 | Repeat("Go", 3) → "GoGoGo" |
strings.Clone(s) |
显式复制(Go 1.18+) | 去共享底层数组 |
注意:
Cut/CutLast返回三个值(before, after string, found bool),多值调用必须完整解构,不能直接塞进fmt.Println这类单值上下文(实测编译错误)。分隔符没找到时before为整个字符串、found为false。
9. 格式化输出(fmt 包核心动词)
| 动词 | 说明 | 适用类型 |
|---|---|---|
%v |
默认格式(万能) | 所有类型 |
%+v |
默认格式 + 结构体字段名 | struct |
%#v |
Go 语法表示 | 所有类型 |
%T |
类型 | 所有类型 |
%d |
十进制整数 | int |
%x / %X |
十六进制(小/大写) | int, string, []byte |
%b |
二进制 | int |
%o |
八进制 | int |
%f |
浮点数 | float |
%e / %E |
科学计数 | float |
%s |
字符串 | string, []byte |
%q |
带引号字符串 | string, rune |
%p |
指针地址 | pointer |
%t |
bool 值 | bool |
%w |
包装 error(仅 fmt.Errorf) |
error |
%% |
字面百分号 | — |
fmt.Printf("type=%T, value=%v\n", x, x)
fmt.Printf("struct with names: %+v\n", myStruct)
fmt.Printf("source code: %#v\n", myStruct)%w 与错误包装
%w 只在 fmt.Errorf 里有意义,把被包装的 error 挂进错误链,供 errors.Is/errors.As 顺藤摸瓜。自 Go 1.20 起支持一条格式串里出现多个 %w,构成多分支错误树(见 07-错误处理.md):
multi := fmt.Errorf("batch failed: %w / %w", errA, errB) // 合法(实测)
// errors.Is(multi, errA) 与 errors.Is(multi, errB) 均为 true旧手册"一条 Errorf 只能用一个 %w"的说法已作废(Go 1.20 起即可多 %w)。另外 go vet(Go 1.27 起)会对 %w 传 *E 而 E 本身实现 error 的情形告警——包指针通常破坏 errors.Is/类型断言,见 11-最佳实践与陷阱.md。
10. 常见陷阱
10.1 len(string) 返回字节数,不是字符数
s := "你好"
fmt.Println(len(s)) // 6(每个中文字符 3 字节)
fmt.Println(utf8.RuneCountInString(s)) // 2(这才是字符数)
// 遍历字符串请用 for range,它会正确解码 UTF-8
for _, r := range s {
fmt.Printf("%c", r) // 你 好
}10.2 字符串索引返回的是 byte,不是 rune
s := "你好"
fmt.Printf("%c\n", s[0]) // ä(乱码!只是 "你" 的第一个 UTF-8 字节)
fmt.Printf("%c\n", []rune(s)[0]) // 你(正确)10.3 iota 在不同 const 块中重置
const (
A = iota // 0
B // 1
)
const (
C = iota // 0(重置了!)
)10.4 := 在嵌套作用域创建新变量
var err error
if true {
result, err := doSomething() // 这是新的 err!外部 err 没被赋值
}
// 外部 err 仍然是 nil!10.5 int 不是 64 位
// 32 位机器上 int = int32,64 位机器上 int = int64
// 需要明确位数的场景(如二进制协议、文件格式)用 int64/uint32 等
var timestamp int64 = time.Now().Unix()10.6 float 比较
a := 0.1
b := 0.2
c := 0.3
fmt.Println(a+b == c) // false! 浮点精度问题
fmt.Println(math.Abs(a+b-c) < 1e-9) // true(正确比较方式)10.7 min/max 不是"兜底",NaN 会传染
v := min(math.NaN(), 1.0) // NaN,不是 1.0(实测)数据源可能产生 NaN 时(如除法未防护),先用 math.IsNaN 判断,再进 min/max。
10.8 clear 不支持数组,也不把 map 置 nil
m := map[string]int{"a": 1}
clear(m)
fmt.Println(m == nil) // false(map 还在,只是空,实测)
var arr [3]int
clear(arr) // 编译错误:argument must be (or constrained by) map or slice(实测)想"清空数组",用 arr = [3]int{} 整体赋零值即可。
实战建议
- 指针带初值一律
new(expr)(Go 1.26+),别再写t := X; &t两行样板;存量代码里x := 30; p := &x也完全合法,new(T)旧用法没有被废弃。 - 取小取大用内建
min/max,多参数直接链式;但注意浮点 NaN 传染,别拿它做兜底钳位。 - 重置 map/slice 用
clear,比for k := range m { delete(m, k) }快且意图清晰;slice 元素含指针时clear还能提前断开引用利于 GC。 - 数字字面量用
0b/0o/0x前缀 +_分隔(Go 1.13+),协议常量、位掩码的可读性提升明显。 - 正则与模板字符串用反引号原始字符串,省掉成串
\\。 - 涉及中文的用户可见字符串,长度、截取一律走
[]rune或utf8.RuneCountInString,只有协议层才按[]byte处理。 - 错误包装放心用多个
%w(Go 1.20+),见07-错误处理.md。