Go语言开发踩坑记录

Go语言开发踩坑记录

用Go开发游戏服务器有一段时间了,从基础语法到Pitaya框架,记录一下踩过的坑和学习心得。

环境搭建

安装配置

1
2
3
4
5
# 查看Go版本
go version

# 查看环境配置
go env

VS Code配置

  1. 安装Go扩展
  2. 配置GOPROXY加速:
1
2
go env -w GOPROXY="https://goproxy.cn,direct"
go env -w GO111MODULE=on
  1. 安装Go工具包:Ctrl+Shift+P → Go: Install/Update Tools

第一个程序

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("你好,世界!")
}

运行:

1
2
3
4
5
go run main.go

# 或编译后运行
go build -o hello main.go
./hello

基础语法踩坑

坑1:变量声明

Go的变量声明有几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 完整声明
var name string = "Go"
var age int = 15

// 类型推断
var version = 1.21

// 简短声明(只能在函数内使用)
isAwesome := true

// 多变量声明
var x, y, z int = 1, 2, 3

注意:=只能在函数内使用,包级别变量要用var

坑2:字符串长度

Go字符串是UTF-8编码,len()返回的是字节数,不是字符数。

1
2
3
4
5
6
str := "Hello, Go语言!"
fmt.Println(len(str)) // 16 (中文占3字节)

// 正确处理中文字符
runes := []rune(str)
fmt.Println(len(runes)) // 12 (按字符计数)

切片和数组的区别

特性 数组 切片
长度 固定 可变
传递方式 值拷贝 引用传递
使用场景 固定大小数据 动态数据集合
1
2
3
4
5
6
7
8
9
10
// 数组(固定长度)
var arr1 [5]int
arr2 := [3]int{1, 2, 3}

// 切片(动态数组)
slice1 := make([]int, 0, 5)
slice2 := []int{1, 2, 3}

// 添加元素
slice1 = append(slice1, 10)

Map使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建map
scores := make(map[string]int)

// 添加元素
scores["Alice"] = 95

// 检查key是否存在
if value, exists := scores["Charlie"]; exists {
fmt.Println("存在:", value)
} else {
fmt.Println("不存在")
}

// 删除元素
delete(scores, "Bob")

// 遍历map
for name, age := range ages {
fmt.Printf("%s: %d\n", name, age)
}

流程控制

Switch特点

Go的switch默认break,不需要显式声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("工作日")
case "Saturday", "Sunday":
fmt.Println("周末")
default:
fmt.Println("无效日期")
}

// 使用fallthrough继续执行下一个case
switch n {
case 1:
fmt.Println("1")
fallthrough
case 2:
fmt.Println("2") // n=1时也会执行这里
}

for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 基本for循环
for i := 0; i < 5; i++ {
fmt.Println(i)
}

// while风格
n := 0
for n < 5 {
fmt.Println(n)
n++
}

// 无限循环
for {
// do something
}

// range遍历
for index, value := range nums {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}

函数与错误处理

多返回值

1
2
3
4
5
6
7
8
9
10
// 多返回值函数
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return // 命名返回值可简写
}

// 使用
q, r := divide(17, 5)
fmt.Printf("商: %d, 余数: %d\n", q, r)

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义错误
var ErrDivideByZero = errors.New("除数不能为零")

func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}

// 使用
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err)
return
}

Panic与Recover

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func mayPanic() {
panic("出现问题了!")
}

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("恢复自:", r)
}
}()

mayPanic()
fmt.Println("这行不会执行")
}

结构体与接口

结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Person struct {
Name string
Age int
Email string
}

// 构造函数风格
type Rectangle struct {
Width float64
Height float64
}

func NewRectangle(width, height float64) *Rectangle {
return &Rectangle{Width: width, Height: height}
}

// 方法定义
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}

接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义接口
type Shape interface {
Area() float64
Perimeter() float64
}

// 隐式实现
type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}

// 多态函数
func PrintShapeInfo(s Shape) {
fmt.Printf("面积: %.2f, 周长: %.2f\n", s.Area(), s.Perimeter())
}

Go的接口是隐式实现,只要实现方法集就是该类型。空接口interface{}可表示任意类型。

并发编程

Goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello")
time.Sleep(100 * time.Millisecond)
}
}

func main() {
go sayHello() // 启动goroutine

time.Sleep(1 * time.Second)
fmt.Println("主程序结束")
}

Channel通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 发送数据
fmt.Println("生产:", i)
}
close(ch) // 关闭channel
}

func consumer(ch <-chan int) {
for num := range ch { // 接收直到channel关闭
fmt.Println("消费:", num)
}
}

func main() {
ch := make(chan int, 3) // 带缓冲的channel
go producer(ch)
consumer(ch)
}

Select多路复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自channel 1"
}()

go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自channel 2"
}()

for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("超时")
}
}

Sync包工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var wg sync.WaitGroup
var mu sync.Mutex
counter := 0

for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}

wg.Wait()
fmt.Println("计数器:", counter)

Go Modules

基本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建模块
go mod init github.com/username/project

# 下载依赖
go mod tidy

# 查看依赖关系
go mod graph

# 下载所有依赖
go mod download

# 获取指定版本
go get github.com/gin-gonic/gin@v1.8.0

# 升级所有依赖
go get -u ./...

go.mod示例:

1
2
3
4
5
6
7
8
module github.com/example/myproject

go 1.21

require (
github.com/gin-gonic/gin v1.9.1
github.com/stretchr/testify v1.8.4
)

Pitaya游戏服务器

环境准备

1
2
3
4
5
6
7
8
9
# 下载etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.5.9/etcd-v3.5.9-linux-amd64.tar.gz
tar -xzf etcd-v3.5.9-linux-amd64.tar.gz
./etcd-v3.5.9-linux-amd64/etcd

# 下载nats-server
wget https://github.com/nats-io/nats-server/releases/download/v2.9.20/nats-server-v2.9.20-linux-amd64.tar.gz
tar -xzf nats-server-v2.9.20-linux-amd64.tar.gz
./nats-server

坑3:依赖版本兼容

Pitaya对etcd和nats版本有要求,要用兼容的版本。另外注意go.mod里的replace指令,有时候需要替换某些依赖。

集群配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func clusterConfig() *config.PitayaConfig {
conf := config.NewDefaultPitayaConfig()

// etcd服务发现
conf.Cluster.Info.Region = "cn-north-1"
conf.Cluster.SD.Etcd.Servers = []string{"http://etcd1:2379"}
conf.Cluster.SD.Etcd.DialTimeoutSec = 5

// NATS消息队列
conf.Cluster.NATS.Connect = "nats://nats-server:4222"
conf.Cluster.NATS.ConnectionTimeout = time.Duration(5 * time.Second)

return conf
}

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func add(a, b int) int {
return a + b
}

func TestAdd(t *testing.T) {
testCases := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
{100, 200, 300},
}

for _, tc := range testCases {
result := add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("add(%d, %d) = %d; expected %d",
tc.a, tc.b, result, tc.expected)
}
}
}

// 基准测试
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
add(100, 200)
}
}

总结

Go语言开发几点心得:

  1. 字符串长度用[]rune转换后再取,避免中文问题
  2. 切片是引用类型,传递时要注意副作用
  3. 接口是隐式实现,不需要显式声明
  4. Channel记得关闭,否则可能goroutine泄漏
  5. Pitaya要注意etcd和nats版本兼容性
  6. 错误处理要习惯,Go没有try-catch

Go的并发模型确实很强大,但也要注意goroutine泄漏和死锁问题。多写测试用例,确保代码质量。