|
### 两种接收方式对比
**方式1:手动判断 `ok` 值**
```go
for {
if elem, ok := <-dataChan; ok {
fmt.Printf("%v\n", elem)
} else {
break // ok == false 说明通道已关闭且缓冲已空
}
}
```
- `elem, ok := <-dataChan` 是"逗号 ok"写法:
- `ok == true`:成功收到一个元素(通道开着,或已关闭但缓冲区还有数据)
- `ok == false`:通道已关闭**且**缓冲区中的数据已被取完,此时 `elem` 是元素类型的零值
- 需要自己写 `if/else` 判断并 `break` 退出
**方式2:`for range` 通道**
```go
for elem := range dataChan {
fmt.Printf("%v\n", elem)
}
```
- 每次循环等价于执行一次 `elem, ok := <-dataChan`
- `ok == true` 时把 `elem` 赋给循环变量、进入循环体
- `ok == false`(通道关闭且取空)时**自动退出循环**,不需要手动 break
两者语义完全等价,`for range` 只是省去了手动判断 `ok` 和 `break` 的样板代码,更简洁、更 Go 风格(idiomatic)。
|
|
### for-range 的行为分析
**你的理解基本正确,精确表述如下**:
1. **通道关闭后**:`for range` 会先把缓冲区中**剩余的数据全部消费完**,然后自动退出循环——不是一关闭就立刻退出。
2. **通道不关闭**:循环在执行下一次 `<-dataChan` 时会**阻塞**在那里,goroutine 挂起等待,直到:
- 有发送方往通道发数据(继续循环),或
- 通道被 `close`(取出剩余数据后退出循环)。
- 若永远没人发送也没人关闭,且没有其他 goroutine 引用该通道,就会发生 **goroutine 泄漏**(goroutine 永久阻塞,GC 无法回收)。
**关键规则(channel 基础语义)**:
| 通道状态 | 接收 `<-ch` | 发送 `ch <-` |
|-----|-----|-----|
| 未关闭、有缓冲数据 | 立即取到数据 | 缓冲未满立即成功,满则阻塞 |
| 未关闭、缓冲为空 | **阻塞**等待发送方 | 缓冲未满立即成功 |
| 已关闭、缓冲还有数据 | 正常取到数据,`ok == true` | **panic**(向已关闭通道发送) |
| 已关闭、缓冲为空 | 立即返回零值,`ok == false`(不阻塞) | **panic** |
所以 `for range` 退出循环的唯一条件是:**通道已 close 且缓冲区已取空**。
**本例中的一个细节**:第二个 goroutine 是先把 3 条数据发进缓冲为 3 的通道(无阻塞),再 `close(dataChan)`,再通过 `startChan` 通知接收方开始收。由于通道已缓冲且已关闭,接收方 `for range` 会一次性把 3 条数据收完然后退出——"先发送、后关闭、再接收"的顺序在这里是安全的。
|
|
### 完整示例代码
```go
func test11() {
dataChan := make(chan int, 3)
startChan := make(chan string, 1)
overChan := make(chan string, 2)
// 接收方:改用 for range
go func() {
<-startChan
fmt.Println("start receive data ")
for elem := range dataChan { // 通道关闭且取空后自动退出
fmt.Printf("%v\n", elem)
}
fmt.Println("reciver over")
overChan <- "rec over"
}()
// 发送方
go func() {
for i := 0; i < 3; i++ {
dataChan <- i
fmt.Sprintf("send data:%v\n", i)
}
fmt.Println("send data over")
// 在接收之前关闭通道
close(dataChan)
fmt.Println("dataChan closed")
// 开始接收
startChan <- "begin"
overChan <- "send data over"
}()
<-overChan
<-overChan
fmt.Println("main over")
}
```
**输出(顺序稳定)**:
```text
send data over
dataChan closed
start receive data
0
1
2
reciver over
main over
```
说明:`overChan <- "rec over"` 和 `overChan <- "send data over"` 谁先到不确定,但 main 会等两个都到才结束,所以 `main over` 一定是最后一行。
|
|
### 结论与注意事项
**结论(回答原问题)**:
- ✅ 是的:`for elem := range dataChan` 在通道**关闭且数据取空**后会自动退出循环,等价于手动 `if elem, ok := <-dataChan; ok {...} else { break }`
- ✅ 是的:如果通道**不关闭**,接收方会阻塞在下一次接收上,一直等到通道被关闭(或收到新数据继续跑)
- ⚠️ 补充:退出前提是"关闭 **且** 取空"——已关闭但缓冲区还有数据时,`for range` 仍会继续把数据收完
**注意事项**:
1. **谁来 close**:惯例是由**发送方**关闭通道,且一个通道只能 close 一次,重复 close 会 panic
2. **关闭后不能发送**:向已关闭的通道发送数据会 panic;关闭 nil 通道也会 panic
3. **goroutine 泄漏**:如果发送方忘记 `close`,接收方的 `for range` 会永久阻塞,造成 goroutine 泄漏——用 `go vet` 或相关工具可检测
4. **接收方不要 close**:接收方关闭通道无法预知是否还有发送方在发,容易 panic
5. `for range` 只适用于**接收**方向;遍历 nil 通道会永远阻塞(`for range nilChan` 直接死锁挂起)
|
|
### 常用函数概览
Go 的字符串是不可变的字节序列,底层为 `[]byte`。标准库 `strings` 提供了丰富的操作函数:
| 函数 | 作用 | 示例 |
|-----|------|------|
| `strings.ReplaceAll(s, old, new)` | 全部替换 | `strings.ReplaceAll("hello", "l", "x")` → `"hexxo"` |
| `strings.Split(s, sep)` | 按分隔符切分 | `Split("a,b,c", ",")` → `["a","b","c"]` |
| `strings.Trim(s, cutset)` | 去除两端指定字符 | `Trim(" go ", " ")` → `"go"` |
| `strings.TrimSpace(s)` | 去除两端空白 | `TrimSpace(" go ")` → `"go"` |
| `strings.HasPrefix/HasSuffix` | 前缀/后缀判断 | |
| `strings.Contains` | 包含判断 | |
| `strings.ToUpper/ToLower` | 大小写转换 | |
> 由于字符串不可变,所有修改操作都会返回新字符串,原字符串保持不变。
|
|
### 替换与分割
**全部替换**
```go
func test13(){
str := "你好"
newStr := strings.ReplaceAll(str, "你", "")
fmt.Println(newStr) // 好
}
```
**按分隔符切分**
```go
arr := strings.Split(s, sep)
```
- `sep` 为空字符串时,按 UTF-8 字符切分
- 结果中不会包含分隔符本身
- 常用变形:`SplitN`、`SplitAfter`
|
|
### 比较与截取
**字符串比较**
```go
func test14(){
// == 直接比较,区分大小写,效率最高
flag := "爱德,我爱你^_^" == "但我不爱你!"
fmt.Println(flag) // false
// strings.Compare 返回 int:0 相等,1 a>b,-1 a<b,区分大小写
n := strings.Compare("abc", "ABC") // 1
// EqualFold 不区分大小写
eq := strings.EqualFold("Go", "go") // true
}
```
**子串截取**
```go
ss := "wa kaka "
// 切片按字节索引,而非字符索引
fmt.Println(ss[3:5]) // ka
ss = "注意力机制让人同一时间只重点关注一个事物"
fmt.Println(ss[0:6]) // 注意
```
> ⚠️ 中文等多字节字符,一个字符可能占 3 个字节,切片时要特别小心。
|
|
### 去除空白
**按指定字符集去除两端**
```go
str := "你好"
newStr := strings.Trim(str, "你")
fmt.Println(newStr) // 好
```
**去除首尾空格**
```go
package main
import (
"fmt"
"strings"
)
func main(){
ss := fmt.Sprintf(" go go go ")
res := strings.TrimSpace(ss)
fmt.Printf("--%v--\n", res) // --go go go--
}
```
**TrimPrefix 与 TrimLeft 的区别**
```go
dirName1 := "/opt/tpf/cwks/src"
p1 := "/opt/tpf/cwks"
// TrimLeft 按字符集逐个去除前缀字符
md_path1 := strings.TrimLeft(dirName1, p1) // "rc"
// TrimPrefix 按完整前缀匹配去除
md_path2 := strings.TrimPrefix(dirName1, p1) // "/src"
fmt.Println(md_path1, md_path2)
```
> `TrimLeft` 的第二个参数被理解为"字符集",会把左侧所有属于该集合的字符去掉;`TrimPrefix` 才是真正的"去前缀"。
|
|
### 易错点总结
1. **字符串不可变**:`s[i] = 'x'` 不允许,修改必须重新赋值
2. **切片按字节**:对中文字符串 `s[0:2]` 可能截断字符,出现乱码
3. **`Trim` vs `TrimPrefix`**:`Trim` 第二个参数是字符集,`TrimPrefix` 第二个参数是完整前缀
4. **空字符串切分**:`strings.Split("", ",")` 返回 `[""]`,而非 `[]`
5. **大小写敏感**:`==` 和 `Compare` 区分大小写,`EqualFold` 不区分
|
|
### 基本用法
Go 正则通过 `regexp` 包实现,常用两步:
1. 编译正则:`re := regexp.MustCompile(pattern)`(编译失败会 panic)
2. 执行匹配或替换
```go
package main
import (
"fmt"
"regexp"
)
func main(){
re := regexp.MustCompile(`\d+`)
fmt.Println(re.FindString("abc123def")) // 123
}
```
> 正则字符串建议用反引号 `` ` `` 包裹,避免对 `\` 进行双重转义。
|
|
### 去除首尾空格
```go
package main
import (
"fmt"
"regexp"
)
func main(){
ss := fmt.Sprintf(" go go go ")
res := regexp.MustCompile(`^\s+|\s+$`).ReplaceAllString(ss, "")
res = fmt.Sprintf("--%v--", res)
fmt.Println(res) // --go go go--
}
```
等价写法:
```go
res := strings.TrimSpace(ss)
```
> 如果只是去首尾空白,优先使用 `strings.TrimSpace`,无需引入正则。
|
|
### 常用正则示例
| 场景 | 正则 | 函数 |
|-----|------|------|
| 匹配邮箱 | `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` | `MatchString` |
| 匹配手机号 | `^1[3-9]\d{9}$` | `MatchString` |
| 提取数字 | `\d+` | `FindAllString` |
| 替换连续空白 | `\s+` | `ReplaceAllString` |
| 验证 IP | `^\d{1,3}(\.\d{1,3}){3}$` | `MatchString` |
```go
re := regexp.MustCompile(`1[3-9]\d{9}`)
if re.MatchString("13800138000") {
fmt.Println("手机号格式正确")
}
```
|
|
### 时间比较
`time.Time` 提供了 `After`、`Before`、`Equal` 方法,可直接比较:
```go
if tim1.After(tim2) {
return 1
} else if tim1.Before(tim2) {
return -1
} else {
return 0
}
```
- `After/Before` 能区分毫秒级差异
- 不建议先转字符串再比较:字符串比较按字典序,`"2023-12-01"` 会大于 `"2023-2-01"`,且月日格式不同容易出错
```go
// 推荐
t1.After(t2)
// 不推荐
t1.Format("2006-01-02") > t2.Format("2006-01-02")
```
|
|
### 时间戳
**获取当前时间戳(秒)**
```go
package main
import (
"fmt"
"time"
)
func main() {
timestamp := time.Now().Unix()
fmt.Println(timestamp) // 1695028544
t := time.Unix(timestamp, 0)
fmt.Println(t) // 2023-09-18 17:15:44 +0800 CST
}
```
| 方法 | 单位 | 说明 |
|-----|------|------|
| `time.Now().Unix()` | 秒 | 常用 |
| `time.Now().UnixMilli()` | 毫秒 | Go 1.17+ |
| `time.Now().UnixMicro()` | 微秒 | Go 1.17+ |
| `time.Now().UnixNano()` | 纳秒 | |
|
|
### 时间加减与格式化
**时间加减**
```go
now := time.Now()
future := now.Add(2 * time.Hour)
past := now.Add(-30 * time.Minute)
// 按日期加减
nextDay := now.AddDate(0, 0, 1)
```
**计算时间差**
```go
d := future.Sub(now)
fmt.Println(d.Minutes()) // 120
```
**格式化输出**
```go
// Go 使用固定参考时间 "2006-01-02 15:04:05"
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format("2006/01/02"))
```
> Go 的格式模板是固定的记忆时间 `Mon Jan 2 15:04:05 MST 2006`。
|
|
### 读取文件
**一次性读取整个文件(推荐)**
```go
package main
import (
"fmt"
"os"
)
func main() {
data, err := os.ReadFile("test.txt")
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println(string(data))
}
```
**按行读取(大文件)**
```go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
f, err := os.Open("test.txt")
if err != nil {
panic(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println(err)
}
}
```
> `ioutil.ReadFile` 已废弃,Go 1.16+ 请用 `os.ReadFile`。
|
|
### 写入文件
**一次性写入**
```go
err := os.WriteFile("test.txt", []byte("hello\n"), 0644)
if err != nil {
panic(err)
}
```
**追加写入**
```go
f, err := os.OpenFile("test.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
defer f.Close()
if _, err := f.WriteString("append line\n"); err != nil {
panic(err)
}
```
**覆盖写入**
```go
f, err := os.Create("test.txt")
if err != nil {
panic(err)
}
defer f.Close()
f.WriteString("new content")
```
| 模式 | 标志 |
|-----|------|
| 只读 | `os.O_RDONLY` |
| 只写 | `os.O_WRONLY` |
| 读写 | `os.O_RDWR` |
| 追加 | `os.O_APPEND` |
| 创建 | `os.O_CREATE` |
| 截断 | `os.O_TRUNC` |
|
|
### 文件信息与遍历
**获取文件信息**
```go
info, err := os.Stat("test.txt")
if err != nil {
panic(err)
}
fmt.Println(info.Name()) // 文件名
fmt.Println(info.Size()) // 大小(字节)
fmt.Println(info.Mode()) // 权限
fmt.Println(info.ModTime()) // 修改时间
```
**判断文件是否存在**
```go
_, err := os.Stat("test.txt")
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
panic(err)
} else {
fmt.Println("文件存在")
}
```
**遍历目录**
```go
entries, err := os.ReadDir(".")
if err != nil {
panic(err)
}
for _, entry := range entries {
fmt.Println(entry.Name(), entry.IsDir())
}
```
|
|
### 路径处理
`path/filepath` 包提供跨平台路径操作:
```go
package main
import (
"fmt"
"path/filepath"
)
func main() {
// 拼接路径(自动处理分隔符)
p := filepath.Join("dir", "sub", "file.txt")
fmt.Println(p) // dir/sub/file.txt
// 获取扩展名
ext := filepath.Ext("a.txt") // .txt
// 获取目录名
dir := filepath.Dir("/a/b/c.txt") // /a/b
// 获取文件名
base := filepath.Base("/a/b/c.txt") // c.txt
// 绝对路径
abs, _ := filepath.Abs("test.txt")
}
```
> 路径操作请使用 `path/filepath`,不要用 `strings.Split` 按 `/` 或 `\` 切分,否则跨平台会出错。
|
|
### 跨平台编译
Go 通过环境变量 `GOOS` 和 `GOARCH` 指定目标平台:
```bash
# Linux 编译 Windows 可执行文件(禁用 CGO,静态链接)
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o app.exe main.go
# Linux 编译 Mac 可执行文件
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o app main.go
```
常用平台组合:
| GOOS | GOARCH | 说明 |
|-----|------|------|
| linux | amd64 | Linux 64 位 |
| windows | amd64 | Windows 64 位 |
| darwin | amd64 | macOS Intel |
| darwin | arm64 | macOS Apple Silicon |
> 禁用 CGO(`CGO_ENABLED=0`)可生成纯静态二进制,部署时无额外依赖。
|
|
### 体积优化
**去除调试信息**
```bash
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -ldflags="-s -w" -o app.exe main.go
```
| ldflags | 作用 |
|---------|------|
| `-s` | 去掉符号表 |
| `-w` | 去掉 DWARF 调试信息 |
**注入版本信息**
```bash
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
-ldflags="-s -w -X main.Version=1.0.0 -X main.BuildTime=$(date +%Y%m%d)" \
-o myapp.exe main.go
```
**UPX 进一步压缩(可选)**
```bash
# 先安装 upx
sudo apt-get install upx
# 压缩
upx --best myapp.exe
```
> UPX 可再减 50%-70% 体积,但部分杀毒软件可能误报。
|
|
### CGO 交叉编译(mingw-w64)
**适用场景**:`CGO_ENABLED=0` 是最简单的交叉编译方式,但工程一旦依赖 cgo 的包(如 `gexpect` 这类调用 C 库的交互库),纯 Go 编译会直接报错。此时需要在 Linux 上安装 mingw-w64 工具链来编译 Windows 目标。
**安装与编译**
```bash
# 安装 mingw-w64
sudo apt-get update
sudo apt-get install mingw-w64
# 使用 mingw 编译(注意:默认生成的 exe 会依赖 DLL)
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 \
go build -o fibu.exe fibu2.go
```
缺点:生成的 `fibu.exe` 需要携带 `libgcc_s_seh-1.dll` 和 `libwinpthread-1.dll`,不是纯静态。
**完全静态化(需额外参数)**
```bash
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 \
go build -ldflags="-s -w -linkmode external -extldflags '-static'" \
-o fibu.exe fibu2.go
```
**验证是否静态链接**
```bash
# Ubuntu 上查看文件类型
file myapp.exe
# 输出:PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
# Windows 上用 dumpbin 查看依赖(静态编译后应只剩 KERNEL32.dll 等系统核心 DLL)
dumpbin /dependents myapp.exe
```
**依赖了 cgo 包时的处理路径**
| 方案 | 说明 |
|-----|------|
| 首选 | 替换为纯 Go 实现,如用 `github.com/google/goexpect` 替代 `gexpect` |
| 次选 | Fork 依赖包自行修改,移除 expect 功能或用条件编译隔离 |
| 备选 | 接受 mingw + DLL 依赖,或把该功能放到 Linux 服务端运行 |
排查 cgo 依赖的命令:
```bash
grep -r "gexpect" /ai/wks/gowks/src/tools/*
go mod graph | grep gexpect
```
|
|
### GOPATH 模式
Go 1.11 之前的工程组织方式,核心规则:
```text
import 路径 = 包目录相对 $GOPATH/src 的路径
```
典型布局:
```text
$GOPATH/
└── src/
├── github.com/user/project/
├── gitee.com/tanpf/tools/
└── cmd/myapp/main.go
```
要点:
- 工程必须放在 `$GOPATH/src` 下
- import 路径通常带域名前缀作为命名空间
- 第三方依赖直接下载到 `$GOPATH/src`,全局唯一版本
- 新版 Go 下需设置 `GO111MODULE=off`
```bash
export GO111MODULE=off
cd $GOPATH/src/cmd/myapp
go build -o myapp main.go
```
|
|
### Go Modules 模式
Go 1.11 引入、1.16 默认开启,当前标准做法:
```text
import 路径 = go.mod 中声明的模块名 + 包相对模块根的路径
```
典型布局:
```text
/project/
├── go.mod # module example.com/demo
├── go.sum
├── main.go
├── pkg/utils/
└── cmd/app/
```
核心命令:
```bash
go mod init example.com/demo # 初始化模块
go mod tidy # 整理依赖
go build ./... # 编译全部包
go run ./cmd/app # 运行指定命令
go test ./... # 运行测试
```
优势:
- 工程可放任意目录
- 每个模块独立管理依赖版本
- import 路径简洁:`example.com/demo/pkg/utils`
|
|
### 选型建议
| 场景 | 推荐方式 |
|-----|---------|
| 新工程 | 一律使用 Go Modules |
| 老工程维护 | 优先迁移到 Modules,或临时 `GO111MODULE=off` |
| 需要跨平台部署 | `CGO_ENABLED=0` 静态编译 |
| 体积敏感 | `-ldflags="-s -w"` + 可选 UPX |
| 需要 CGO | 使用 mingw-w64 等交叉编译工具链,但会依赖 DLL |
迁移老工程示例:
```bash
cd /wks/gowks/src
go mod init gowks
go mod tidy
# 将 gitee.com/tanpf/tools 改为 gowks/tools
# 或用 replace 过渡:replace gitee.com/tanpf/tools => ./tools
```
> 现代 Go 开发应以 Go Modules 为主,GOPATH 模式仅用于理解遗留项目。
|