2.1 变量声明

Go 的硬性规则:包级作用域只能有「声明」,不能有「语句」

类别 例子 能写在哪
声明(declaration) varconstfunctype 函数外 ✅ 函数内 ✅
语句(statement) x = 5iffor、函数调用 只能写在函数内 ❌ 函数外

Go 有三种声明变量的方式,按使用频率排序:

方式一:短变量声明 :=(最常用)

name := "张三"      // Go 自动推断 name 是 string
age := 28           // 自动推断 age 是 int
pi := 3.14          // 自动推断 pi 是 float64

只能在函数内部使用(不能用在包级别)。这是你日常写代码 90% 的场景。

注意:在同一个作用域里,只要左边至少有一个新变量,:= 就不算是"重新声明",而是给新变量做声明,给已存在的变量做赋值。

对比 Node.js

// Node.js
let name = "张三"     // 没有类型,可以随时改成别的类型
const age = 28

// Go
name := "张三"
// name = 28          // ❌ 编译错误!name 已是 string,不能赋 int

方式二:var 声明(带类型或不带类型)

var name string          // 只声明,不赋值(得到零值 "")
var age int = 28         // 声明 + 赋值 + 显式类型
var pi = 3.14            // 声明 + 赋值,类型自动推断

var 可以用在包级别(函数外面),这是它的主要用途:

package main

var globalCount = 0      // 包级变量,必须用 var

func main() {
    localName := "张三"   // 函数内可以用 :=
}

方式三:批量声明

多个变量一起声明:

var (
    host    = "localhost"
    port    = 8080
    debug   = true
)

或短变量批量:

x, y, z := 1, 2, 3             // 同时声明三个
a, b := compute()              // 接收函数的多个返回值(第 03 章详讲)

💡 上面这些声明里出现了 stringintfloat64bool 等类型名,如果你还不太清楚它们具体是什么、有什么区别,下一节 2.2 会逐一介绍


2.2 基本数据类型

Go 是静态类型语言,每个变量都有明确的类型。下面是 Go 的基本类型全览。

整数类型

Go 提供了非常细的整数类型(不像 JS 只有一个 Number):

类型 范围 用途
int8 -128 ~ 127 很小的整数
int16 -32768 ~ 32767
int32 -21亿 ~ 21亿
int64 非常大 时间戳、大数
int 平台相关(64位机 = int64) 日常默认用这个
uint8 ... uint64 无符号(只有正数和 0) 位运算、二进制
byte = uint8 的别名 二进制数据
rune = int32 的别名 Unicode 字符(重要!)

经验法则:除非有特殊原因(性能优化、协议格式),统一用 int。Go 会在 64 位机器上自动用 64 位。

⚠️ 注意:intint64 是不同的类型,不能直接运算,必须转换(见 2.4 节)。这是 Go 强类型的核心体现。

浮点类型

var f1 float32 = 3.14
var f2 float64 = 3.141592653589793   // 默认精度,日常用这个

默认 f := 3.14 推断为 float64(不是 32)。日常用 float64 即可。

布尔类型

var b bool = true
var c bool         // 零值是 false

⚠️ Go 的布尔不能和数字互转

// Node.js:if (1) { ... }    // true
// Go:
var count = 5
if count { }       // ❌ 编译错误,必须写 if count > 0

字符串

s := "hello"
s2 := "你好"
s3 := '你'  // 单引号只能1个字符

// 字符串是不可变的(和 JS 一样)
// s[0] = 'H'     // ❌ 编译错误

// 多行字符串(类似 JS 模板字符串的原始版)
msg := `第一行
第二行
第三行`

rune vs byte —— UTF-8 字符串处理(重要)

Go 字符串底层是 UTF-8 字节序列。对中文这种多字节字符,处理时要小心:

s := "hello, 世界"

// ❌ 错误做法:按字节遍历
fmt.Println(len(s))               // 13(字节数,不是字符数!)
// h-e-l-l-o-,-空格-世(3字节)-界(3字节) = 13 字节

// ✅ 正确做法:转成 []rune 按字符处理
runes := []rune(s)				  // 转成字符串Unicode 码点组成的切片
fmt.Println(len(runes))           // 9(真正的字符数)

for i, r := range s {             // range 字符串时自动按 rune 遍历
    fmt.Printf("%d: %c\n", i, r)
}

对比 Node.js:JS 字符串是 UTF-16,"世界".length 是 2。Go 是 UTF-8,len("世界") 是 6。这个差异在处理中文时要特别注意。

[]rune(s) 的作用是将字符串s的内容转成由字符串每个字符的Unicode 码点组成的切片,比如:[36825 26159 20013 22269],会占用内存,如果字符串很大,会有性能开销。因此更好的方式是:直接用标准库 utf8.RuneCountInString(s),它直接计算字符数,不分配内存

import "unicode/utf8"
s := "hello, 世界"
fmt.Println(utf8.RuneCountInString(s)) // 输出 9,且不占用额外内存

类型一览速查

类别 类型 典型用途
整数 intint8/16/32/64uint 系列 默认用 int
浮点 float32float64 默认用 float64
布尔 bool(只有 true/false 条件判断
字符串 string(UTF-8,不可变) 文本
字符 rune(= int32,Unicode 码点) 遍历字符
字节 byte(= uint8) 二进制数据

复合类型(数组、切片、map、结构体等)在第 04、05 章详讲。


2.3 零值(Zero Value)

Go 中变量声明了就有默认值,不存在「未定义」状态。这和 JS 的 undefined 不同。

类型 零值
int, float 等数值 0
bool false
string ""(空字符串)
指针、切片、map、函数、接口、channel nil
struct 每个字段的零值
var n int        // 0
var s string     // ""
var b bool       // false
fmt.Println(n, s, b)   // 0  false

好处:Go 没有空指针解引用的「未初始化」问题。一个变量永远有确定的值。

注意:Go 的 nil 和 JS 的 null/undefined 不是一回事。nil 只用于引用类型(指针、切片、map 等),数值和字符串永远不会是 nil


2.4 类型转换详解(重要)

现在你已经认识了 Go 的所有基本类型(2.2 节)和它们的零值(2.3 节),本节讲这些类型之间怎么互相转换

2.4.1 为什么 Go 没有隐式类型转换?

先看 Node.js 代码:

let x = 1
let y = "2"
console.log(x + y)   // "12",字符串拼接
console.log(x * y)   // 2,字符串转数字
console.log("5" == 5)   // true,等号也会转换

这种自动类型转换是 JS 一大堆 bug 的根源:

  • '0' == false 到底是 true 还是 false?
  • [] + {} 等于什么?
  • parseInt("10px") 返回 10 还是 NaN?

Go 彻底杜绝了这个坑——所有类型转换必须显式写出来,否则编译报错:

x := 1       // int
y := "2"     // string

// x + y          // ❌ 编译错误:mismatched types int and string
// x * y          // ❌ 编译错误
// x == "1"       // ❌ 编译错误
z := x + 2        // ✅ 都是 int,类型一致,可以

核心规则一句话Go 永远不会自动帮你转换类型,即使看起来「很明显」也不行。这看起来啰嗦,但换来的是编译期就消灭一整类 bug

2.4.2 类型转换的工具一览

Go 的类型转换分几大类,工具不一样:

转换场景 工具 例子
数值之间(int↔float↔uint) 内置函数 T(x) float64(42)
数字 ↔ 字符串 strconv strconv.Itoa(42)
字符串 ↔ 字节切片 []byte(s) []byte("abc")
通用格式化 fmt.Sprintf fmt.Sprintf("%d", x)
接口类型 → 具体类型 类型断言 .(T) 见第 06 章

下面分类详细讲。

2.4.3 数值之间的转换:T(x) 语法

格式:目标类型(值)任何数值类型之间都可以转

var i int = 42

// int → float
f := float64(i)            // 42.0

// float → int(小数部分被截断!不是四舍五入)
n := int(3.9)              // 3,不是 4!
n2 := int(-3.9)            // -3

// int ↔ uint
var u uint = uint(i)       // 42
var i2 int = int(u)        // 42

// 不同位宽互转
var i64 int64 = int64(i)   // int → int64
var i32 int32 = int32(i64) // int64 → int32(注意下面陷阱)

⚠️ 三个必须知道的陷阱

陷阱 1:float 转 int 是「截断」不是「四舍五入」

int(2.9)    // 2(不是 3)
int(-2.9)   // -2

// 想要四舍五入:用 math.Round
n := int(math.Round(2.9))  // 3

陷阱 2:大类型转小类型可能溢出(高位截断)

var i64 int64 = 9999999999   // 超过 int32 范围(约 21 亿)
var i32 int32 = int32(i64)   // 不是 9999999999,而是截断后的乱数!
fmt.Println(i32)             // 1410065407(诡异的结果)

陷阱 3:负数转 uint 会变成超大的正数

var i int = -1
var u uint = uint(i)
fmt.Println(u)   // 18446744073709551615(uint64 最大值)

这是因为负数在内存里是补码,转无符号数后被解释成超大正数。

2.4.4 整数 ↔ 字符串:strconv 包

数字转字符串strconv.Itoa(Itoa = Integer to ASCII):

import "strconv"

s := strconv.Itoa(42)      // "42"
s2 := strconv.Itoa(-100)   // "-100"

字符串转整数strconv.Atoi(Atoi = ASCII to Integer):

import "strconv"

n, err := strconv.Atoi("42")    // n=42, err=nil
n2, err := strconv.Atoi("4.2")  // n2=0, err=错误(不是整数)
n3, err := strconv.Atoi("abc")  // n3=0, err=错误
n4, err := strconv.Atoi("42px") // n4=0, err=错误(不像 JS 会解析出 42)

// 注意:Atoi 返回两个值——转换结果 和 错误
// err 是 Go 表示「有没有出错」的内置类型:nil 表示没出错,不是 nil 表示出错了
if err != nil {            // 第 07 章会系统讲错误处理,现在先记住:err != nil 就是出错了
    fmt.Println("转换失败:", err)
}

⚠️ 对比 JS 的 parseInt("42px") 会返回 42(解析到非数字为止),Go 的 Atoi 整个字符串必须是合法数字才会成功,否则报错。Go 更严格。

2.4.5 浮点数 ↔ 字符串

strconv.FormatFloatstrconv.ParseFloat

// 浮点 → 字符串
s := strconv.FormatFloat(3.14, 'f', 2, 64)  // "3.14"
//                                       ↑ 64 表示 float64 位宽

// 更常用:用 fmt.Sprintf(见 2.4.8)
s2 := fmt.Sprintf("%.2f", 3.14159)   // "3.14"

// 字符串 → 浮点
f, err := strconv.ParseFloat("3.14", 64)
if err != nil {
    fmt.Println("解析失败")
}

2.4.6 字符串 ↔ 字节切片(常见于 IO 操作)

Go 的字符串底层就是字节序列,和 []byte 可以互转:

s := "hello"
b := []byte(s)         // [104 101 108 108 111]
s2 := string(b)        // "hello"

// 什么时候用?读文件、网络传输、哈希计算时
data := []byte("hello world")
hash := sha256.Sum256(data)

⚠️ 注意:[]byte(s)复制一份数据(字符串不可变,切片可变),频繁转换有性能开销。

2.4.7 rune ↔ string(Unicode 字符)

runeint32 的别名,代表一个 Unicode 码点:

// rune → string:得到「这个码点对应的字符」
var r rune = '中'           // 单引号包围单个字符 = rune
s := string(r)              // "中"

// 字符串中的某个字符
str := "Hello, 世界"
for _, r := range str {     // range 字符串自动按 rune 遍历
    fmt.Printf("%c", r)     // H e l l o , (空格) 世 界
}

// string → []rune:得到「字符切片」
runes := []rune("世界")     // [19990 30028]
fmt.Println(len(runes))     // 2(两个字符)
fmt.Println(len("世界"))     // 6(六个字节!UTF-8 编码)

⚠️ 关键区分len("世界") 是 6(字节数),len([]rune("世界")) 是 2(字符数)。处理中文时一定要用 rune。

2.4.8 通用格式化:fmt.Sprintf(万能但稍慢)

任何类型都能用 fmt.Sprintf 转字符串(类似 JS 的模板字符串):

import "fmt"

// 数字转字符串
s := fmt.Sprintf("%d", 42)        // "42"
s2 := fmt.Sprintf("%.2f", 3.14)   // "3.14"
s3 := fmt.Sprintf("%t", true)     // "true"

// 拼接多种类型
msg := fmt.Sprintf("用户 %s,%d 岁,余额 %.2f 元", "张三", 28, 99.5)
// "用户 张三,28 岁,余额 99.50 元"

Sprintf vs strconv.Itoa 怎么选?

  • 只转单个 int → 用 strconv.Itoa更快
  • 需要拼接多个值/格式化 → 用 fmt.Sprintf(更方便)

2.4.9 不能转换的情况(编译报错)

这些转换 Go 不支持,会编译错误:

// ❌ bool 不能和数字互转
var b bool = bool(1)          // 编译错误
var n int = int(true)         // 编译错误

// ❌ string 不能直接转数字(必须用 strconv)
var n int = int("42")         // 编译错误(int("42") 会被当成码点转换!)
// ↑ 注意:string(42) 是合法的,但意思是「码点 42 对应的字符」,不是 "42"!
fmt.Println(string(42))       // "*"(ASCII 码 42 是星号)

⚠️ 特别坑string(42) 不会报错,但结果是 "*"(ASCII 42 是星号),不是 "42"!新手最容易踩。要转成 "42" 必须用 strconv.Itoa(42)

2.4.10 类型转换速查表

从 → 到 方法 例子
int → float64 float64(i) float64(42) → 42.0
float64 → int int(f)(截断) int(3.9) → 3
int → int64 int64(i) int64(42)
int → string strconv.Itoa(i) strconv.Itoa(42) → "42"
string → int strconv.Atoi(s) strconv.Atoi("42") → (42, nil)
float64 → string fmt.Sprintf("%f", f) fmt.Sprintf("%.2f", 3.14)
string → float64 strconv.ParseFloat(s, 64)
string → []byte []byte(s) []byte("abc")
[]byte → string string(b) string([]byte{97}) → "a"
rune → string string(r) string('中') → "中"
string → []rune []rune(s) len([]rune("世界")) → 2
任意 → string fmt.Sprintf("%v", x) 万能但稍慢
bool ↔ int 不支持 需要手动 if 判断

💡 记忆口诀

  • 同类数值T(x) 直接转
  • 数字和字符串strconv 包(Itoa / Atoi)
  • 啥都搞不定fmt.Sprintf(万能兜底)

2.5 常量 const

const Pi = 3.14159
const AppName = "我的博客"
const MaxRetries = 3

// 批量声明
const (
    StatusOK = 200
    StatusNotFound = 404
    StatusError = 500
)

对比 Node.js:几乎一样,const 也是编译期常量。

iota —— Go 特有的枚举机制

Go 没有 enum 关键字,用 const + iota 实现枚举:

const (
    Sunday    = iota    // 0
    Monday              // 1(iota 自动递增)
    Tuesday             // 2
    Wednesday           // 3
    Thursday            // 4
    Friday              // 5
    Saturday            // 6
)

iota 在每个 const 块里从 0 开始,每行 +1。

更实用的例子:用位运算做权限系统

先解释 <<:它是「左移运算符」,把数字的二进制往左移几位。1 << iota 就是 1 左移 iota 位:

1 << 0 = 1   二进制 0001  (iota=0,左移 0 位,还是 1)
1 << 1 = 2   二进制 0010  (iota=1,左移 1 位)
1 << 2 = 4   二进制 0100  (iota=2,左移 2 位)

注意结果是 1、2、4(不是 1、2、3)。这种「每个权限占独立的一位」的设计,好处是可以组合

const (
    Read    = 1 << iota    // 1  (0001)
    Write                  // 2  (0010)
    Execute                // 4  (0100)
)

// 组合权限:用 |(按位或)
perm := Read | Write       // 3 (0011) = 同时有读和写权限

// 判断权限:用 &(按位与)
if perm & Read != 0 {       // 0011 & 0001 = 0001 ≠ 0 → 有读权限
    fmt.Println("可读")
}
if perm & Execute != 0 {    // 0011 & 0100 = 0000 = 0 → 无执行权限
    fmt.Println("可执行")
}

// 加权限
perm |= Execute             // 0011 | 0100 = 0111,现在三个权限都有了

// 移除权限
perm &^= Write              // 0111 &^ 0010 = 0101,移除了写权限

为什么用位运算而不用 1、2、3? 因为位运算可以一个变量存多个权限Read|Write = 3 就同时表示两个权限),而普通的 1、2、3 做不到。这是 Unix 文件权限(chmod 755)、Linux 能力系统的底层原理。


2.6 Node.js ↔ Go 对照表

概念 Node.js Go 备注
变量声明 let a = 1 a := 1 := 只能函数内
常量 const PI = 3.14 const PI = 3.14 几乎一样
类型推断 自动(动态) 编译期推断(静态) Go 推断后类型固定
类型可变 $a = 1; $a = "x" ❌ 不允许 Go 类型固定
类型转换 自动(弱类型) 必须显式 数值用 T(x),数字↔字符串用 strconv
int → string String(42) strconv.Itoa(42) Go 不能直接转
string → int parseInt("42px") → 42 strconv.Atoi("42") Go 严格匹配整个字符串
浮点 → int Math.floor(3.9) 或自动 int(3.9) → 3 Go 是截断
数字格式化 模板字符串 `${x}` fmt.Sprintf("%d", x) Go 用占位符
bool ↔ 数字 Number(true) → 1 ❌ 不支持 需手动 if 判断
空值 undefined / null 零值(0/""/false) Go 变量总有值
数字类型 Number 一种 int/float/uint 多种 Go 精细区分
字符串长度 "世界".length === 2 len("世界") === 6 Go 是 UTF-8 字节数
布尔与数字 if (1) 合法 if 1 ❌ 错误 Go 严格区分
枚举 无原生(用对象/常量) const + iota Go 特有