2022年初,我们团队开始从 PHP 向 Go 迁移核心服务。作为一个写了 10 年 PHP 的开发者,学 Go 的过程中有很多感触——Go 的简洁、并发模型、编译型语言的特性,和 PHP 差异很大。今天把我的学习路径和踩坑经验分享出来,给想从 PHP 转 Go 的朋友做个参考。
一、为什么学 Go# 性能 :Go 编译型,性能比 PHP 快 5-10 倍,内存占用低并发 :goroutine + channel,高并发编程比 PHP-FPM 模式高效得多部署简单 :编译成单个二进制文件,不用装运行环境,容器镜像小生态成熟 :Gin、Gorm、gRPC 等框架成熟,云原生时代的主流语言团队需求 :核心服务要从 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 项目的标准布局:
m y a c i p c g g p m n k o o o p d s t h s r m g n . . / / e e a e e o / f m s r m r n r p d i o u v a n d v o e g d m e i a l i s l s r n l e c i / / / . / r e t g / / o o r y / # # # # # # # # 主 内 H 业 数 数 可 配 程 部 T 务 据 据 对 置 序 包 T 逻 访 模 外 文 入 ( 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 的学习路径:
语法基础 :变量、类型、函数、结构体、接口、错误处理、并发工程实践 :项目结构、依赖管理、配置、日志、测试Web 开发 :Gin + Gorm,实现 CRUD、中间件、参数校验微服务 :gRPC、服务注册发现、配置中心、链路追踪生产实践 :性能调优、并发安全、错误处理、部署运维Go 和 PHP 是两种不同范式的语言,Go 的简洁、显式、并发模型需要时间适应。但一旦掌握,你会发现 Go 写出来的代码更稳定、性能更好、部署更简单。对于 PHP 开发者来说,Go 是一门值得学习的语言,尤其是在云原生和微服务时代。
我学 Go 花了大概 3 个月就能独立开发微服务,半年后开始主导核心服务的 Go 迁移。PHP 的基础(面向对象、Web 开发、数据库)对学 Go 很有帮助,很多概念是相通的,只是实现方式不同。不要怕转语言,编程的核心是解决问题的思维,语言只是工具。