2.1 变量声明
Go 的硬性规则:包级作用域只能有「声明」,不能有「语句」
类别 例子 能写在哪 声明(declaration) var、const、func、type函数外 ✅ 函数内 ✅ 语句(statement) x = 5、if、for、函数调用只能写在函数内 ❌ 函数外
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 章详讲)
💡 上面这些声明里出现了
string、int、float64、bool等类型名,如果你还不太清楚它们具体是什么、有什么区别,下一节 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 位。
⚠️ 注意:
int和int64是不同的类型,不能直接运算,必须转换(见 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,且不占用额外内存
类型一览速查
| 类别 | 类型 | 典型用途 |
|---|---|---|
| 整数 | int、int8/16/32/64、uint 系列 |
默认用 int |
| 浮点 | float32、float64 |
默认用 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.FormatFloat 和 strconv.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 字符)
rune 是 int32 的别名,代表一个 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 特有 |
💬 评论 0
还没有评论,快来抢沙发吧~