2022年初,我们团队开始从 PHP 向 Go 迁移核心服务。作为一个写了 10 年 PHP 的开发者,学 Go 的过程中有很多感触——Go 的简洁、并发模型、编译型语言的特性,和 PHP 差异很大。今天把我的学习路径和踩坑经验分享出来,给想从 PHP 转 Go 的朋友做个参考。

一、为什么学 Go

  1. 性能:Go 编译型,性能比 PHP 快 5-10 倍,内存占用低
  2. 并发:goroutine + channel,高并发编程比 PHP-FPM 模式高效得多
  3. 部署简单:编译成单个二进制文件,不用装运行环境,容器镜像小
  4. 生态成熟:Gin、Gorm、gRPC 等框架成熟,云原生时代的主流语言
  5. 团队需求:核心服务要从 PHP 迁移到 Go,必须掌握

二、语法差异:PHP vs Go

2.1 变量与类型

Go 是静态类型,变量声明时指定类型:

1
2
3
4
5
6
7
// Go
var name string = "Allen"
age := 30 // 短变量声明,自动推断类型

// PHP
$name = "Allen";
$age = 30;

Go 的类型系统比 PHP 严格,不会自动类型转换。"3" + 5 在 PHP 里是 8,在 Go 里编译报错。这一开始很不适应,但减少了很多隐性 bug。

2.2 错误处理

Go 没有异常,用多返回值处理错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Go
result, err := doSomething()
if err != nil {
    return fmt.Errorf("failed: %w", err)
}

// PHP
try {
    $result = doSomething();
} catch (Exception $e) {
    throw new Exception("failed: " . $e->getMessage());
}

一开始觉得 Go 的错误处理很啰嗦,到处都是 if err != nil。但后来发现这种显式错误处理让代码更清晰,错误不会被意外吞掉,调试起来更容易。

2.3 面向对象

Go 没有类,用结构体 + 方法实现面向对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Go
type User struct {
    ID   int
    Name string
}

func (u *User) GetName() string {
    return u.Name
}

// PHP
class User {
    public $id;
    public $name;
    
    public function getName() {
        return $this->name;
    }
}

Go 没有继承,用组合替代继承。接口是隐式实现的,不需要 implements 关键字:

1
2
3
4
5
6
7
8
type Reader interface {
    Read(p []byte) (n int, err error)
}

type File struct{}
func (f *File) Read(p []byte) (n int, err error) { // 隐式实现 Reader 接口
    return 0, nil
}

2.4 并发

Go 的并发是最大的亮点,goroutine 比 PHP 的多进程轻量太多:

1
2
3
4
5
6
7
8
// Go:启动 1000 个 goroutine,内存占用几 MB
for i := 0; i < 1000; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

// PHP:启动 1000 个进程,内存占用几个 GB,基本不可行

channel 用于 goroutine 间通信:

1
2
3
4
5
6
7
8
ch := make(chan int, 10) // 带缓冲的 channel

go func() {
    ch <- 42 // 发送
}()

value := <-ch // 接收
fmt.Println(value)

三、工程实践

3.1 项目结构

Go 项目的标准布局:

myacipcggpmnkooopdsthsrmgn..//eeaeeo/fmsrmrnrpdiouvandvoegdmeialislsrnleci///./retg//oory/########HTT访P

3.2 依赖管理

Go Modules 管理依赖,类似 Composer:

1
2
3
go mod init github.com/example/myapp  # 初始化,类似 composer init
go get github.com/gin-gonic/gin       # 安装依赖,类似 composer require
go mod tidy                             # 整理依赖,类似 composer install

3.3 Web 开发:Gin + Gorm

PHP 用 Yii2/Laravel,Go 用 Gin(Web 框架)+ Gorm(ORM):

 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
// main.go
package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    // 连接数据库
    db, _ := gorm.Open(mysql.Open("user:pass@tcp(localhost:3306)/db"), &gorm.Config{})

    r := gin.Default()

    r.GET("/users/:id", func(c *gin.Context) {
        var user User
        db.First(&user, c.Param("id"))
        c.JSON(200, gin.H{"code": 0, "data": user})
    })

    r.Run(":8080")
}

Gin 的性能比 PHP 框架快很多,QPS 能到 1 万+,而 PHP-FPM 模式一般只有 1000-2000。

3.4 配置管理

Go 用 viper 管理配置,支持多格式:

1
2
3
4
5
6
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
viper.ReadInConfig()

port := viper.GetString("server.port")

四、微服务架构

4.1 gRPC 通信

Go 是 gRPC 的一等公民,跨服务通信用 gRPC 比 HTTP+JSON 高效:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// user.proto
syntax = "proto3";
service UserService {
    rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
    int64 id = 1;
}
message User {
    int64 id = 1;
    string name = 2;
}
1
protoc --go_out=. --go-grpc_out=. user.proto

4.2 服务注册与发现

用 etcd 或 Consul 做服务注册发现,类似 PHP 里的 Nginx upstream,但更动态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 服务注册
client, _ := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
lease, _ := client.Grant(context.Background(), 5)
client.Put(context.Background(), "/services/user/1", "127.0.0.1:8080", clientv3.WithLease(lease.ID))

// 保持租约
ch, _ := client.KeepAlive(context.Background(), lease.ID)
go func() {
    for range ch {}
}()

五、踩坑经验

1. nil pointer dereference

Go 的 nil 指针访问会 panic,不像 PHP 访问 null 的属性只是警告。一定要检查指针是否为 nil,特别是函数返回指针的时候。

2. goroutine 泄漏

goroutine 启动后如果没有正确退出,会泄漏。比如 channel 没有关闭、goroutine 阻塞在 IO 上。要用 context 控制 goroutine 生命周期,超时自动取消。

3. 循环变量捕获

for 循环里启动 goroutine,捕获循环变量会出问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 错误:所有 goroutine 可能都打印最后一个值
for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(i) // 捕获的是变量 i,不是值
    }()
}

// 正确:作为参数传入
for i := 0; i < 10; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

4. defer 的执行顺序

defer 是 LIFO(后进先出),多个 defer 的执行顺序和声明顺序相反。而且 defer 的参数是在声明时求值的,不是执行时。

5. 接口的 nil 判断

接口类型的 nil 判断要小心,接口包含类型和值两部分,类型不为 nil 但值为 nil 时,接口不等于 nil:

1
2
3
var p *MyType = nil
var i interface{} = p
fmt.Println(i == nil) // false!因为类型是 *MyType,不是 nil

六、学习资源推荐

  • 官方教程:A Tour of Go(go.dev/tour),交互式学习,入门首选
  • 书籍:《Go 程序设计语言》(The Go Programming Language),经典教材
  • 实战项目:读 Gin、Gorm、etcd 等开源项目的源码,学习最佳实践
  • 社区:Go 官方博客、Golang China 社区、r/golang

七、总结

从 PHP 转 Go 的学习路径:

  1. 语法基础:变量、类型、函数、结构体、接口、错误处理、并发
  2. 工程实践:项目结构、依赖管理、配置、日志、测试
  3. Web 开发:Gin + Gorm,实现 CRUD、中间件、参数校验
  4. 微服务:gRPC、服务注册发现、配置中心、链路追踪
  5. 生产实践:性能调优、并发安全、错误处理、部署运维

Go 和 PHP 是两种不同范式的语言,Go 的简洁、显式、并发模型需要时间适应。但一旦掌握,你会发现 Go 写出来的代码更稳定、性能更好、部署更简单。对于 PHP 开发者来说,Go 是一门值得学习的语言,尤其是在云原生和微服务时代。

我学 Go 花了大概 3 个月就能独立开发微服务,半年后开始主导核心服务的 Go 迁移。PHP 的基础(面向对象、Web 开发、数据库)对学 Go 很有帮助,很多概念是相通的,只是实现方式不同。不要怕转语言,编程的核心是解决问题的思维,语言只是工具。