Go 数据类型基础
这篇笔记用于入门 Go 语言的数据类型。
刚开始学习 Go 时,最容易混淆的是:byte 和 rune、数组和切片、nil 和零值、map 和结构体、接口和普通类型。先把这些基础类型理清楚,后面学习函数、方法、并发和 Web 开发会顺很多。
Go 的数据类型可以先分成两大类:
Go 数据类型├─ 基本数据类型│ ├─ 数值类型│ │ ├─ 整数类型│ │ ├─ 浮点类型│ │ └─ 复数类型│ ├─ 布尔类型│ └─ 字符串类型└─ 派生数据类型 / 复合数据类型 ├─ 指针 ├─ 数组 ├─ 切片 ├─ map ├─ 结构体 ├─ 函数 ├─ 接口 └─ 通道可以先记住一句话:
基本类型用来表示简单值,复合类型用来组织多个值或表达更复杂的关系。
基本数据类型
Section titled “基本数据类型”Go 的整数类型分为有符号整数和无符号整数。
| 类型 | 说明 |
|---|---|
int | 有符号整数,长度和平台有关 |
int8 | 8 位有符号整数 |
int16 | 16 位有符号整数 |
int32 | 32 位有符号整数 |
int64 | 64 位有符号整数 |
uint | 无符号整数,长度和平台有关 |
uint8 | 8 位无符号整数 |
uint16 | 16 位无符号整数 |
uint32 | 32 位无符号整数 |
uint64 | 64 位无符号整数 |
uintptr | 用于保存指针地址的整数类型 |
byte | uint8 的别名,常用于表示字节 |
rune | int32 的别名,常用于表示 Unicode 字符 |
示例:
package main
import "fmt"
func main() { var age int = 18 var count uint = 100 var b byte = 'A' var r rune = '你'
fmt.Println(age) fmt.Println(count) fmt.Println(b) // 65 fmt.Println(r) // 20320}这里要注意:
byte本质是uint8,适合处理字节流。rune本质是int32,适合处理中文、emoji 等 Unicode 字符。int和uint的长度和平台有关,普通业务里常用int即可。
Go 有两种常用浮点类型:
| 类型 | 说明 |
|---|---|
float32 | 单精度浮点数 |
float64 | 双精度浮点数 |
示例:
package main
import "fmt"
func main() { var price float64 = 19.99 var rate float32 = 0.85
fmt.Println(price) fmt.Println(rate)}实际开发中,如果没有特殊要求,通常使用 float64。
需要注意:浮点数不适合直接表示精确金额。涉及金额时,常见做法是使用整数保存分,或者使用 decimal 类库。
Go 也提供复数类型:
| 类型 | 说明 |
|---|---|
complex64 | 实部和虚部都是 float32 |
complex128 | 实部和虚部都是 float64 |
示例:
package main
import "fmt"
func main() { var value complex128 = 1 + 2i
fmt.Println(value) fmt.Println(real(value)) fmt.Println(imag(value))}普通后端开发很少直接用复数类型,但它属于 Go 的数值类型。
布尔类型只有两个值:
truefalse
示例:
package main
import "fmt"
func main() { var isLogin bool = true var isDeleted bool = false
fmt.Println(isLogin) fmt.Println(isDeleted)}Go 中不能把数字当作布尔值使用。
// 错误写法// if 1 {// }判断条件必须是布尔表达式。
字符串类型是 string。
package main
import "fmt"
func main() { var name string = "xiaoxi" message := "你好,Go"
fmt.Println(name) fmt.Println(message)}Go 字符串是不可变的。
如果需要频繁拼接字符串,可以使用 strings.Builder。
package main
import ( "fmt" "strings")
func main() { var builder strings.Builder
builder.WriteString("hello") builder.WriteString(" ") builder.WriteString("go")
fmt.Println(builder.String())}字符:byte 和 rune
Section titled “字符:byte 和 rune”Go 没有单独的 char 类型。
通常用:
byte表示一个字节。rune表示一个 Unicode 字符。
示例:
package main
import "fmt"
func main() { text := "Go语言"
fmt.Println(len(text)) // 字节长度
for i := 0; i < len(text); i++ { fmt.Printf("%c\n", text[i]) }
for _, ch := range text { fmt.Printf("%c\n", ch) }}这里 len(text) 返回的是字节长度,不是字符数量。
如果要按字符遍历,应该使用 range,它会按 rune 处理。
Go 中变量声明后,如果没有显式赋值,会有默认零值。
| 类型 | 零值 |
|---|---|
| 数字类型 | 0 |
| 布尔类型 | false |
| 字符串 | "" |
| 指针 | nil |
| 切片 | nil |
| map | nil |
| channel | nil |
| interface | nil |
| 函数 | nil |
| 结构体 | 每个字段都是对应类型零值 |
示例:
package main
import "fmt"
func main() { var age int var name string var ok bool var scores []int
fmt.Println(age) // 0 fmt.Println(name) // "" fmt.Println(ok) // false fmt.Println(scores) // [] fmt.Println(scores == nil)}零值是 Go 里很重要的设计。很多类型即使没有显式初始化,也能处于一个可预测状态。
派生数据类型 / 复合数据类型
Section titled “派生数据类型 / 复合数据类型”指针用于保存变量的内存地址。
package main
import "fmt"
func main() { age := 18 p := &age
fmt.Println(age) fmt.Println(p) fmt.Println(*p)
*p = 20 fmt.Println(age)}这里:
&age获取变量地址。p保存地址。*p通过指针访问地址中的值。
Go 有指针,但不支持像 C 那样的指针运算。
数组是固定长度的同类型元素集合。
package main
import "fmt"
func main() { var nums [3]int = [3]int{1, 2, 3}
fmt.Println(nums) fmt.Println(nums[0])}数组长度是类型的一部分。
var a [3]intvar b [4]int
// a 和 b 是不同类型所以 Go 中直接使用数组的场景相对少,更多时候使用切片。
切片是动态长度的序列,是 Go 中最常用的数据结构之一。
package main
import "fmt"
func main() { nums := []int{1, 2, 3}
nums = append(nums, 4)
fmt.Println(nums) fmt.Println(len(nums)) fmt.Println(cap(nums))}切片有三个重要信息:
- 指向底层数组的指针。
- 长度
len。 - 容量
cap。
数组和切片区别:
| 对比项 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定 | 动态 |
| 类型是否包含长度 | 包含 | 不包含 |
| 使用频率 | 较少 | 很常用 |
| 扩容 | 不支持 | append 可能触发扩容 |
map 是键值对集合。
package main
import "fmt"
func main() { user := map[string]int{ "xiaoxi": 18, "veyliss": 20, }
age, ok := user["xiaoxi"] if ok { fmt.Println(age) }
user["new"] = 22 delete(user, "veyliss")
fmt.Println(user)}读取 map 时,经常使用 value, ok 判断 key 是否存在。
value, ok := user["name"]需要注意:未初始化的 nil map 不能直接写入。
package main
func main() { user := make(map[string]int) user["xiaoxi"] = 18}结构体用于组织多个字段,适合描述一个对象或实体。
package main
import "fmt"
type User struct { ID int Name string Age int}
func main() { user := User{ ID: 1, Name: "xiaoxi", Age: 18, }
fmt.Println(user.Name)}结构体经常用于:
- 表示业务实体。
- 接收 JSON。
- 数据库模型。
- 配置对象。
函数也是一种类型,可以赋值给变量,也可以作为参数传递。
package main
import "fmt"
func calculate(a int, b int, fn func(int, int) int) int { return fn(a, b)}
func main() { add := func(a int, b int) int { return a + b }
result := calculate(1, 2, add) fmt.Println(result)}函数类型常用于回调、策略函数、中间件等场景。
接口定义一组方法。一个类型只要实现了接口要求的方法,就自动实现了这个接口。
package main
import "fmt"
type Speaker interface { Speak() string}
type User struct { Name string}
func (u User) Speak() string { return "hello, " + u.Name}
func say(s Speaker) { fmt.Println(s.Speak())}
func main() { user := User{Name: "xiaoxi"} say(user)}Go 的接口是隐式实现的,不需要写 implements。
这点和 Java 很不一样。
通道也叫 channel,用于 goroutine 之间通信。
package main
import "fmt"
func main() { ch := make(chan string)
go func() { ch <- "hello" }()
message := <-ch fmt.Println(message)}通道体现了 Go 并发里很重要的思想:
不要通过共享内存来通信,而要通过通信来共享内存。
后面学习 goroutine、并发控制、生产者消费者模型时,会经常用到 channel。
Go 不会自动进行隐式类型转换。
package main
import "fmt"
func main() { var a int = 10 var b int64 = 20
result := int64(a) + b
fmt.Println(result)}即使都是整数类型,int 和 int64 也需要显式转换。
这让 Go 的类型边界更清楚,但刚开始写时会觉得啰嗦。
| 易混点 | 说明 |
|---|---|
byte 和 rune | byte 是字节,rune 是 Unicode 字符 |
| 数组和切片 | 数组长度固定,切片长度动态 |
| nil map | nil map 可以读,但不能写 |
| 字符串长度 | len(string) 返回字节数,不是字符数 |
| interface | 接口是隐式实现,不需要显式声明 |
| channel | 用于 goroutine 通信,不是普通队列的完全替代 |
| 类型转换 | Go 要求显式类型转换 |
运行与编译基础
Section titled “运行与编译基础”学完基础类型后,可以顺手记住 Go 程序最常用的运行和编译命令。
Go 有两个高频命令:
go run:直接运行 Go 程序,适合开发和测试。go build:编译 Go 程序,生成可执行文件。
运行 Go 程序
Section titled “运行 Go 程序”运行指定文件:
go run main.go运行当前目录下的整个包:
go run .如果项目里有多个 .go 文件,并且它们属于同一个 package main,通常更推荐使用 go run .。
编译 Go 程序
Section titled “编译 Go 程序”编译指定文件:
go build main.go编译当前目录下的整个包:
go build也可以显式写成:
go build .指定输出文件名:
go build -o app main.go常见命令可以这样记:
| 命令 | 作用 |
|---|---|
go run main.go | 运行指定 Go 文件 |
go run . | 运行当前目录下的包 |
go build main.go | 编译指定 Go 文件 |
go build | 编译当前目录下的包 |
go build . | 编译当前目录下的包 |
go build -o app main.go | 编译并指定输出文件名 |
交叉编译指的是:在当前系统上编译出另一个系统可运行的程序。
常见会用到三个环境变量:
| 变量 | 作用 |
|---|---|
GOOS | 目标操作系统,例如 linux、darwin、windows |
GOARCH | 目标 CPU 架构,例如 amd64、arm64 |
CGO_ENABLED | 是否启用 CGO,纯 Go 项目通常可以设为 0 |
在 PowerShell 中,可以这样交叉编译。
编译到 Linux 64 位:
$env:GOOS="linux"; $env:GOARCH="amd64"; $env:CGO_ENABLED="0"; go build -o output-linux main.go编译到 macOS 64 位:
$env:GOOS="darwin"; $env:GOARCH="amd64"; $env:CGO_ENABLED="0"; go build -o output-macos main.go编译到 Windows 64 位:
$env:GOOS="windows"; $env:GOARCH="amd64"; $env:CGO_ENABLED="0"; go build -o output-windows.exe main.go如果是在 macOS 或 Linux 的 shell 中,可以这样写:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o output-linux main.goGOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o output-macos main.goGOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o output-windows.exe main.go如果项目依赖 CGO,例如依赖某些 C 库,交叉编译会更复杂,不能简单地把 CGO_ENABLED 设置为 0。普通 Go 入门项目大多可以先按纯 Go 项目处理。
Go 数据类型可以先记成两类:
基本类型:数字、布尔、字符串复合类型:指针、数组、切片、map、结构体、函数、接口、通道入门阶段最重要的是:
- 会区分
byte和rune。 - 会区分数组和切片。
- 会使用
map存键值对。 - 会用结构体组织业务数据。
- 理解接口是隐式实现。
- 知道 channel 用于 goroutine 通信。
- 会使用
go run、go build运行和编译基础程序。
这些内容掌握后,Go 的基础语法就有了比较稳的地基。