《Go语言高级开发与实战》系列01:Go语言的基础语法 从零到一掌握核心数据类型与高级技巧
本文基于《Go语言高级开发与实战》第1章内容,系统梳理Go语言的基础语法、变量常量、流程控制、字符串实战、数组与切片、map、结构体、接口、模块管理及通道等核心知识点。通过大量可运行代码示例和实战案例,帮助读者建立扎实的Go语言基本功,并掌握实际开发中的常用技巧。
一、Go语言基础
1.1 基础语法
1. Go程序标记
Go程序由关键字、标识符、常量、字符串、符号等多种标记组成。
2. 行分隔符
Go语言中,一行就是一个语句,不需要在行尾加分号。例如:
fmt.Println("Hi,Gopher,Let's Go!")
fmt.Println("Go语言高级开发与实战")3. 注释
- 单行注释:以
//开头 - 多行注释(块注释):以
/*开头,以*/结尾
4. 标识符
- 由字母(A~Z, az)、数字(09)、下划线
_组成 - 首字符不能是数字,也不能是Go关键字
- 用于命名变量、类型等程序实体
5. 字符串连接
使用 + 运算符连接字符串。
6. 关键字(25个)
| 关键字 | 关键字 | 关键字 | 关键字 | 关键字 |
|---|---|---|---|---|
| break | default | func | interface | select |
| case | defer | go | map | struct |
| chan | else | goto | package | switch |
| const | fallthrough | if | range | type |
| continue | for | import | return | var |
预定义标识符(30多个):
- 常量:
true、false、iota、nil - 类型:
int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr、float32、float64、complex128、complex64、bool、byte、rune、string、error - 函数:
make、len、cap、new、append、copy、close、delete、complex、real、imag、panic、recover
7. 空格
变量声明必须使用空格隔开,建议使用编辑器格式化工具自动处理。
1.2 变量
1. 声明
Go是静态类型语言,使用 var 关键字声明变量,类型放在变量名之后。
var name type例如声明整型指针:
var x, y *int零值:声明后自动赋予默认值(int→0,float→0.0,bool→false,string→"",指针→nil)。
命名规则:遵循驼峰命名法(首个单词小写,后续单词首字母大写),非强制。
三种声明形式:
标准格式:
var 变量名 变量类型批量格式:
var ( userId int username string score float32 )简短格式:
名字 := 表达式- 只能用于函数内部(不能声明全局变量)
- 不能提供数据类型,同时显式初始化
- 可同时声明多个变量:
username, goodAt := "Shirdon", "Golang"
2. 赋值
单个变量:
var name type = value多个变量:
var name1, name2, name3 = value1, value2, value3交换变量:
x, y = y, x
3. 作用域
- 局部变量:函数体内声明,包括参数和返回值
- 全局变量:函数体外声明,可被整个包甚至外部包(导出后)使用
1.3 常量
1. 声明
const 常量名 [类型] = 常量值
const e = 2.7182818282. 常量生成器 iota
iota 用于生成一组以相似规则初始化的常量。在 const 声明中,第一个 iota 为 0,之后每个常量声明行加 1。
1.4 运算符
Go语言运算符优先级(数值越大优先级越高):
| 优先级 | 分类 | 运算符 | 结合性 |
|---|---|---|---|
| 14 | 后缀 | ( )、[ ]、-> | 从左到右 |
| 13 | 单目 | !、*(指针)、&、++、--、+、- | 从右到左 |
| 12 | 乘法/除法/取余 | *、/、% | 从左到右 |
| 11 | 加法/减法 | +、- | 从左到右 |
| 10 | 位移动 | <<、>> | 从左到右 |
| 9 | 关系 | <、<=、>、>= | 从左到右 |
| 8 | 相等/不等 | ==、!= | 从左到右 |
| 7 | 按位与 | & | 从左到右 |
| 6 | 按位异或 | ^ | 从左到右 |
| 5 | 按位或 | | | 从左到右 |
| 4 | 逻辑与 | && | 从左到右 |
| 3 | 逻辑或 | || | 从左到右 |
| 2 | 赋值 | =、+=、-=、*=、/=、%=、>>=、<<=、&=、^=、|= | 从右到左 |
| 1 | 逗号 | , | 从左到右 |
1.5 流程控制语句
1. if…else(分支结构)
- 条件必须是布尔型或逻辑型
{必须与if或else在同一行- 前段代码块的
}必须与else在同一行(编译器强制)
2. for循环
三种形式:
// 形式1:和C语言一样
for init; condition; post { }
// 形式2:和C的while一样
for condition { }
// 形式3:无限循环
for { }注意事项:
- 左大括号
{必须与for同一行 - 不支持逗号间隔的多赋值语句,使用平行赋值
- 支持
break、continue,并提供更高级的break可中断指定循环
3. for…range循环
Go特有,可遍历数组、切片、字符串、map、channel。
for key, val := range 复合变量值 {
// 循环体
}4. switch…case
- 表达式不必为常量或整数
- case 之间独立,不需要
break跳出 - 一分支多值:
case "Jack", "Barry": - 分支表达式:switch 后不加变量,case 后跟条件表达式
5. goto语句
跳转到标签,可用于快速跳出多重循环。
func main() {
for x := 0; x < 5; x++ {
for y := 0; y < 5; y++ {
if y == 2 {
goto breakTag
}
}
}
return
breakTag:
fmt.Println("done")
}6. break语句
可结束 for、switch、select 代码块。支持标签,退出指定标签对应的代码块。
7. continue语句
结束当前循环,开始下一次迭代。仅限 for 循环内使用。支持标签。
二、字符串实战技巧
2.1 字符串基础
- 字符串是字节的定长数组,值不可变
- 采用 UTF-8 编码,英文字符占1字节,中文字符占3字节
- 与其他语言(如Java固定2字节)不同,Go的UTF-8编码节省内存和磁盘空间
2.2 中文字符串的截取
直接使用切片截取中文可能导致乱码(截断多字节编码)。正确做法:先转为 []rune 类型,截取后再转回 string。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
str := "在Go中可以通过切片截取一个数组或字符串"
fmt.Println(utf8.RuneCountInString(str)) // 20 (字符数)
fmt.Println(len(str)) // 56 (字节数)
// 直接切片截取前9个字节(可能乱码)
str1 := str[0:9]
fmt.Println(str1) // 输出: 在Go中
// 正确方式:转为[]rune
nameRune := []rune(str)
fmt.Println(len(nameRune)) // 20
fmt.Println("string = ", string(nameRune[0:9])) // 在Go中可以通过切
}2.3 按单词或字节反转字符串
先将字符串转为 []rune,利用平行赋值反转,再转回 string。
func Reversal(a string) (re string) {
b := []rune(a)
for i := 0; i < len(b)/2; i++ {
b[i], b[len(b)-i-1] = b[len(b)-i-1], b[i]
}
re = string(b)
return
}
// 示例:输入 "123456789abc" 输出 "cba987654321"2.4 生成随机字符串
方法1:math/rand包(伪随机)
import "math/rand"
import "time"
func GetRandomString(l int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyz"
bytes := []byte(str)
result := []byte{}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < l; i++ {
result = append(result, bytes[r.Intn(len(bytes))])
}
return string(result)
}方法2:crypto/rand包(密码学安全随机)
import "crypto/rand"
func NewLenChars(length int, chars []byte) string {
// 实现略,见原书
}方法3:哈希值表示随机字符串
使用 crypto/md5 包,结合时间戳生成。
2.5 控制大小写
- 转换大小写:
strings.ToUpper()/strings.ToLower() - 区分大小写替换:使用
regexp.MustCompile("(?i)" + v)和ReplaceAllString
2.6 去除字符串首尾空格
strings.TrimSpace(s)strings.Trim(s, cutset)
2.7 生成CSV数据
使用 encoding/csv 包:
os.Create()创建文件- 写入 UTF-8 BOM:
f.WriteString("\xEF\xBB\xBF") csv.NewWriter(f)获取 Writerw.Write(record)写入记录
2.8 解析CSV数据
file, err := os.Open("test.csv")
reader := csv.NewReader(file)
reader.Comma = ';' // 设置分隔符
for {
record, err := reader.Read()
if err != nil {
break
}
fmt.Println(record)
}2.9 获取中文字符串
使用正则表达式 regexp.MustCompile("^[\u4e00-\u9fa5]+") 匹配中文。
2.10 按指定函数分割字符串
strings.FieldsFunc(s, func(rune) bool) 可根据自定义函数分割。
2.11 合并与分割字符串
- 合并:
strings.Join(elems []string, sep string) - 分割:
Split(s, sep)→ 切片SplitN(s, sep, n)→ 限制片数SplitAfter(s, sep)→ 保留分隔符SplitAfterN(s, sep, n)
2.12 按指定函数截取字符串
strings.TrimFunc(s, func(rune) bool) 截取字符串两端满足条件的字符。
2.13 【实战】生成可下载的CSV文件
完整 Web 服务示例:
http.HandleFunc("/down", Welcome)
http.ListenAndServe(":8088", nil)
func Welcome(w http.ResponseWriter, r *http.Request) {
filename := "exportUsers.csv"
// 生成CSV内容...
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
w.Write(content)
}2.14 【实战】用Go运行Shell脚本程序
使用 os/exec 包:
cmd := exec.Command("tr", "a-z", "A-Z")
cmd.Stdin = strings.NewReader("I love golang")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
fmt.Printf("translated phrase: %q\n", out.String())三、数组和切片基础技巧
3.1 数组和切片基础
数组:长度固定,不可改变
切片(Slice):动态数组,长度不固定,可追加元素
len:当前长度(已赋值最大下标+1)cap:容量(当前可容纳元素个数)
切片是引用类型,传递时共享底层数组
切片结构:包含指针(指向底层数组)、len、cap
切片创建方式:
make([]Type, length, capacity) make([]Type, length) []Type{} []Type{value1, value2, ...}
3.2 迭代处理数组
使用 for range:
for k, v := range array {
fmt.Printf("index:%d value:%d address:%X\n", k, v, &array[k])
}注意:v 是副本,地址不变;要取元素地址需用 &array[k]。
3.3 从数组中删除元素
利用切片特性:将被删除元素前后两部分连接。
seq := []string{"i", "love", "go", "advanced", "programming"}
index := 2
seq = append(seq[:index], seq[index+1:]...)
// 结果: [i love advanced programming]3.4 将数组转换为字符串
- 单个元素直接赋值
- 遍历拼接:
strings.Join或手动+
3.5 检查某个值是否在数组中
遍历比较,返回布尔值。
3.6 查找元素在数组中的位置
使用 reflect.ValueOf 获取值,遍历比较。
3.7 查找数组中最大值或最小值
遍历比较并记录下标。
3.8 随机打乱数组(Fisher-Yates洗牌算法)
func RandomInt(strings []string, length int) (string, error) {
for i := len(strings) - 1; i > 0; i-- {
num := rand.Intn(i + 1)
strings[i], strings[num] = strings[num], strings[i]
}
// 返回前length个
return strings.Join(strings[:length], ""), nil
}3.9 删除数组中重复的元素
双指针法(前提数组已排序):
func removeDuplicates(array []int) []int {
if len(array) == 0 {
return nil
}
left, right := 0, 1
for ; right < len(array); right++ {
if array[left] == array[right] {
continue
}
left++
array[left] = array[right]
}
return array[:left+1]
}四、数组高级技巧
4.1 一维数组的排序
使用 sort 包:
a := []int{4,3,2,1,5,6}
sort.Sort(sort.Reverse(sort.IntSlice(a))) // 递减排序4.2 二维数组的排序
实现 sort.Interface 接口,自定义 Less 方法按指定列排序。
4.3 多维数组声明
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
// 示例:3行4列的二维数组
array := [3][4]int{{2,3,4}, {6,7,8}, {9,10,11,12}}4.4 多维数组遍历
使用多重 for 循环嵌套。
4.5 多维数组的查询
通过索引逐级访问,如 mArray[0][0][0]。
4.6 多维数组的修改
直接对指定索引重新赋值。
4.7 三维数组生成器
func make3D(m, n, p int) [][][]float64 {
buf := make([]float64, m*n*p)
x := make([][][]float64, m)
for i := range x {
x[i] = make([][]float64, n)
for j := range x[i] {
x[i][j] = buf[:p:p]
buf = buf[p:]
}
}
return x
}4.8 【实战】从数据库获取数据并合并处理和导出
结合 database/sql、encoding/csv 和 MySQL 驱动,实现:
- 连接数据库
- 查询多张表
- 使用 goroutine 并发处理
- 导出为 CSV 文件
五、map常见实战技巧
5.1 map基础
- 键值对的无序集合,称为“关联数组”或“字典”
- 引用类型
- 声明:
var name map[key_type]value_type - 未初始化的 map 值为
nil,使用前需make
5.2 检查键是否在map中
if val, ok := map[key]; ok {
// 存在
}5.3 json与map互相转化
- JSON 转 map:
json.Unmarshal([]byte(jsonStr), &mapResult) - map 转 JSON:
json.Marshal(mapInstance)
5.4 map排序
- 按 key 排序:将 key 取出放入切片,排序后遍历
- 按 value 排序:使用
sort.Slice自定义比较函数 - 按字符频率降序:构建
frequency结构体切片,用sort.Slice排序
5.5 map高级使用技巧
- 多维 map:
map[KeyType1]map[KeyType2]...ValueType - 注意 nil map 写操作会 panic,必须初始化
- map 类型的切片:需要两次
make(先切片,再每个元素 make map)
5.6 map排序技巧
// 按value降序排序
sort.Slice(keys, func(i, j int) bool {
return counts[keys[i]] > counts[keys[j]]
})5.7 【实战】从数据库导出特定CSV文件
- 定义结构体
User - 查询数据库返回
[]User - 将数据转换为二维字符串切片
- 使用
encoding/csv写入文件
六、结构体的使用技巧
6.1 结构体基础
由一系列具有相同或不同类型的数据构成的集合
字段特性:拥有自己的类型和值、字段名唯一、字段类型可以是结构体自身
定义格式:
type 类型名 struct { 字段1 类型1 字段2 类型2 }访问权限:首字母大写为公有(Public),小写为私有(Private)
6.2 结构体初始化
四种方式:
// 方法1:声明后赋值
var person Programmer
person.Name = "Jack"
// 方法2:按顺序初始化
person1 := Programmer{"Jack", "PHP"}
// 方法3:new
var person2 = new(Programmer)
person2.Name = "Barry"
// 方法4:取地址
var person3 = &Programmer{}
person3.Name = "Shirdon"6.3 结构体继承
Go 没有 extends,通过组合实现继承:
type Father struct { Name string }
type Child struct {
Father // 匿名字段
}
// Child 可直接调用 Father 的方法
child := Child{}
child.Study() // 实际是 child.Father.Study()6.4 结构体组合
“has a”关系:外部结构体包含内部结构体作为字段。
6.5 匿名结构体
没有名字的结构体,常用于临时数据封装。
6.6 结构体嵌套
常用于解析 API 响应等场景。
6.7 结构体字段标签(Tag)
格式:key1:"value1" key2:"value2",可通过反射获取。
type Programmer struct {
Name string `json:"name" level:"12"`
}
// 反射获取标签
field, _ := reflect.TypeOf(pro).FieldByName("Name")
fmt.Println(field.Tag.Get("json")) // name6.8 Go语言面向对象编程
- 封装:使用结构体封装属性,方法定义在接收者上
- 继承:通过结构体内嵌匿名类型实现
- 多态:通过接口实现
6.9 【实战】模拟二维矢量移动小游戏
- 实现
Vector结构体,提供 Add、Sub、Multi、DistanceTo、Normalize 方法 - 实现
Player结构体,包含 CurrentVector、TargetVector、Speed - 在
Update中计算方向、单位化、移动 - 主循环不断更新位置直到到达目标
七、接口常用技巧
7.1 接口基础
接口定义了一组方法,但不实现。接口本质是指针类型,可实现多态。
type 接口名称 interface {
method1(参数列表) 返回值列表
method2(参数列表) 返回值列表
}7.2 接口的赋值
- 将实现接口的对象实例赋值给接口(需实现所有方法)
- 将一个接口赋值给另一个接口(方法列表为子集即可)
7.3 接口的查询
使用类型断言:value, ok := interfaceVar.(ConcreteType) 使用 switch type 判断接口存储的具体类型。
7.4 接口的组合
接口可以嵌套组合:
type InterfaceCombine interface {
Interface1
Interface2
// 自定义方法
}7.5 接口的常见应用
- 类型推断:将接口变量还原为原始类型
- 实现多态:函数参数为接口类型,传入不同实现执行不同行为
- 作为不确定类型的参数:使用空接口
interface{}配合类型开关
7.6 接口使用注意事项
- 可变参数是空接口类型时,注意展开切片:
fmt.Println(arr...) - 空指针和空接口不等价:
var err *os.PathError = nil返回的 error 接口不是 nil
八、Go语言模块管理
从 Go 1.11 开始支持模块(module),Go 1.16 默认开启 GO111MODULE=on。
常用命令
| 命令 | 说明 |
|---|---|
go mod download | 下载依赖到本地缓存 |
go mod edit | 编辑 go.mod 文件 |
go mod graph | 打印模块依赖图 |
go mod init | 初始化新模块,创建 go.mod |
go mod tidy | 添加缺失的模块,移除未使用的模块 |
go mod vendor | 复制依赖到 vendor 目录 |
go mod verify | 校验依赖 |
go mod why | 解释为什么需要依赖 |
创建 module
go mod init modulename生成 go.mod 文件,内容如:
module modulename
go 1.16添加依赖
- 在 Go 1.16 之前:运行
go build或go test会自动下载并添加 - Go 1.16 及之后:需手动运行
go get下载依赖
升级依赖
- 主版本升级:
go get package@version(主版本号变化需改变模块路径,如/v2) - 次版本/补丁升级:
go get -u或go get -u=patch
移除依赖
删除代码中的 import 后,运行 go mod tidy 自动从 go.mod 移除。
九、通道实战技巧
9.1 通道基础
通道(channel)用于 goroutine 之间通信,遵循 先入先出(FIFO) 规则。
ch := make(chan string)
go hello(ch)
ch <- "World"
close(ch)9.2 创建定时通知
func TimeLong(d time.Duration) <-chan struct{} {
ch := make(chan struct{}, 1)
go func() {
time.Sleep(d)
ch <- struct{}{}
}()
return ch
}实际开发中建议使用 time.After()。
9.3 使用通道传送通道
通道的元素可以是另一个通道,实现复杂的控制流。
9.4 检查通道的长度和容量
cap(ch):容量len(ch):当前元素个数(注意:长度值不一定是准确的,因为可能正在变化)
9.5 速率限制
使用带缓冲的通道和 time.Tick 实现:
quotas := make(chan time.Time, RateLimit)
go func() {
tick := time.NewTicker(RateLimitPeriod / RateLimit)
for t := range tick.C {
select {
case quotas <- t:
default:
}
}
}()十、回顾与启示
本章系统讲解了 Go 语言基础数据类型、字符串、数组切片、map、结构体、接口、模块管理、通道等核心知识,并通过大量实战案例展示了这些技术的实际应用场景。掌握这些基础技巧,是深入学习 Go 语言并发编程、网络编程、微服务开发等的坚实基石。
