via:<br>
https://medium.com/@joesweeny/from-php-to-go-74c1f896c4dc
<br>
作者:Joe Sweeny
四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!
一篇源自 Medium 的文章,点赞 300+。
原文如下:
写这篇文章的时候 PHP 最新版本是 PHP7.4,Go 的最新版本是 Go1.13。
直到 2018 年 11 月份,我的开发经验主要是 PHP,外加一点 Vue。在开发生涯早期的时候,我一直在加强自己的 PHP 相关技能,现在是时候拓宽自己的技术栈了。采纳了一位同事的建议,开始学习另外一门开发语言。
我对服务端开发感觉会更舒服,所以我应该把注意力集中在前端开发还是应该学习另一种服务器端语言?有人建议我学习另外一门服务端开发语言。这样做不仅可以增加自己工作机会,作为一名软件开发人员,还有利于提高自己的能力并改进自己编写 PHP 代码的方式。
在这样的建议下,我决定学习 Go 语言。在 Medium 和一些流行开发论坛里阅读了大量文章,发现 Go 有很高的人气,所以我就很自然地觉得选择 Go 是一个正确的选择。
我深入研究了一些博客论坛、YouTube学习视频、Udemy 课程和其他各种文献,但我发现由于我的 PHP 背景,进入 Go 领域的难度相当大。我自己经常会比较 PHP 与 Go 之间的异同点,以便降低学习 Go 语言的难度。所以我将这些异同点整理成文章,以便可以帮助那些与我有相同处境的人。
Go 社区和生态系统有提供丰富的学习文档、示例和教程,特别适合初学者入门。这篇文章的主要目的是深入探讨一些常见的编程概念,并提供一些优秀的对比示例,以帮助 PHP 开发人员更顺利地进入 Go 开发。
### 动态类型 vs 静态类型
PHP 是一门动态语言,尽管从 PHP7.0 版本开始它被描述成一种渐进式语言。总体而言,PHP 是一种具有某些静态类型的动态语言。使用 PHP 的开发人员可以灵活地遵守严格的约定,比如函数参数的类型提示和返回类型声明。但另一方面,开发人员可以选择不遵守任何严格的类型约定,仍然可以开发出一个功能完整的程序。
PHP 允许开发人员动态地声明某种类型确定的变量,然后可以用不同类型覆盖之前的变量。
```PHP
<?php
$error = true;
$message = null;
if ($error) {
$message = 'You have encountered an error';
// Original variable type of null now cast to a string
}
echo $message; // 'You have encountered an error'
```
即使使用严格的类型声明,PHP 仍允许在函数内使用新类型分配变量。下面提供了一个严格类型的函数声明示例,尽管该函数中并未完全遵守严格类型:
```PHP
<?php
declare(strict_types=1);
/**
* @param bool $error
* @param null $message
* @return string
*/
function printError(bool $error, $message = null): ?string
{
if ($error) {
$message = 'You have encountered an error';
// Initial null variable now contains a string value
// even with strict type enforcement
}
return $message;
}
$message = printError(true, null);
echo $message; // 'You have encountered an error'
```
另外一方面,Go 是一种严格类型的静态类型语言。变量一旦在声明的时候分配了某种类型,
就不能转化成其他类型或者赋值给其他类型的变量。尝试这样做将会导致编译报错。
```go
package errors
// Short variable assignement
err := false
// Inferred type assignment
var message string
if err == false {
message = nil
//Compiler error: cannot use nil as type string in assignment
}
```
返回值类型也是严格的并且函数参数必须有类型声明。上面 PHP 的示例允许 null 或者 string 作为返回类型,然而 Go 的返回值类型是非常严格的,返回值类型声明之后是不能改变的。上面的 PHP 示例用 Go 语言的方式实现:
```go
package errors
import (
"fmt"
)
func printError(err bool, m string) string {
if err {
message = "You have encountered an error"
// Attempting to set 'message' to anything other than a string will result
// in a compiler error
}
return message
}
m := printError(true, "")
fmt.Println(m) // "You have encountered an error"
```
Go 语言的严格类型约定给了我们一种“所得即所见”的感觉。减少了进入函数内部检查类型的必要并且提供了在弱类型语言中所缺乏的程序鲁棒性和健壮性。
我个人使用 PHP 时一般采用严格类型声明,我经常利用静态分析库(例如 PHPStan)在我的项目中实施这种方法,所以对于我来说,进入 Go 这种严格类型语言并没有太多的不适应。
另外一方面,如果你更喜欢使用 PHP 来实现更灵活的功能,那么你需要接受更严格的方法。PHP7.4 的类型属性使得 PHP 更接近 Go 的严格类型约定,因此如果 PHP 开发人员选择在他们的项目中使用这个约定,那么从 PHP 转到 Go 的障碍就会有所减少。
### Class vs Struct
PHP 中类可以实例化出对象,这些对象包含内部作用域的变量属性,类行为在方法中定义。在 Go 里面类称为 struct,是用户定义的类型,它包含具有声明数据类型的字段集合。PHP 和 Go 的对比示例如下:
```php
<?php
class Person
{
/**
* @var string
*/
public $firstName;
/**
* @var string
*/
public $lastName;
/**
* @var int
*/
public $age;
public function __construct(string $firstName, string $lastName, int $age)
{
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->age = $age;
}
}
```
```go
package people
type Person struct {
FirstName string
FastName string
Age int
}
```
PHP强制使用其必需的属性实例化类,而 Go 中的 struct 类型可以通过多种方式实例化。
```go
package people
// Declares a variable of type 'Person'. The internal fields of the p variable
// are set to their zero value i.e. FirstName is an empty string and Age is 0
var p Person
// Instantiate a struct by supplying the value of all the struct fields.
var p = Person{"Joe", "Sweeny", 36}
// Initialize a struct by supplying name: value pairs of all the struct fields.
var p = Person{FirstName: "Joe", LastName: "Sweeny", Age: 36}
```
### 函数/方法声明
与 PHP 类似,函数是 Go 语言的一等公民,也是生态系统中的核心,但有一个关键区别,Go 函数支持多返回值。
PHP 开发人员如果想要返回多个值,可以将多个值组成单个数组返回或者返回包含多个值的对象。然而,Go 提供的是更清晰、更优雅、更易读的解决方案。
```go
package main
func SumProductDiff(i, j int) (int, int, int) {
return i+j, i*j, i-j
}
```
PHP 中类里面定义的函数称为方法,在 Go 语言里具有接收者参数的函数称为方法。Go 支持在类型上定义方法。
```go
package people
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
func (p Person) FullName() string {
return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}
```
上面的例子可以理解为,FullName() 方法的接收者类型是 Person。
### 命名空间 vs 包
PHP 使用命名空间作为管理项目的方式,这也有助于避免命名冲突。命名空间还为 PHP 代码库提供了一定程度的组织性,命名空间通常采用目录结构。为了使用不同类中的函数,可以使用 use 关键字来导入所需要的类、接口和函数。
```php
<?php
namespace App\Domain\Entity\User;
use App\Domain\Persistence\Repository;
```
Go 使用了模块化系统,将代码中的逻辑组织到一个包中。可以通过 import 语句导入外部包。
```go
package user
import (
"github.com/joesweeny/app/persistence/repository"
)
```
如果你之前使用过 Python 或者 Javascript,那对这种模块化系统会比较熟悉。如果你是直接从 PHP 转过来的话,就需要适应这种项目结构和组织代码的方式。
通过命名空间处理包是我必须克服的一个障碍。使用 PHP 的时候,我通常采用分层的架构思维来使用命名空间,在层与层之间创建清晰的逻辑边界。在包命名方面,尽管我发现 Brian Ketelsen 的精彩演讲在一定程度上能帮我克服这个障碍,但最主要还是心态的转变。
### 可见性
PHP 中属性的可见性,无论是方法、常量还是变量,都可以通过在声明前添加关键字 public、protected 或 private 来定义。声明为 public 的类成员可以从任何地方访问,private 成员只能由定义它们的类访问,protected 成员可以被继承该类的子类访问。
```php
<?php
class Visibility
{
protected string $protected = 'Protected';
private string $private = 'Private';
public function public(): string
{
return 'Access from me anywhere';
}
}
```
对于可见性,Go 语言采用了一种更简单的方法。Go 不支持继承,而是选择组合、嵌入和接口来支持代码重用和多态。可见性变为 public 或 private。在 Go 中,类型、变量、函数或方法的可见性用首字母大写声明为 public,用首字母小写声明为 private。
```go
package greeter
var hello bool
func SayHello(h bool) {
hello = h
}
func PrintHello() string {
if !hello {
return 'Goodbye'
}
return 'Hello'
}
```
在上面的例子中,SayHello 和 PrintHello 函数在 greeter 包中声明。使用 greeter 包的外部代码将接收 SayHello 和 PrintHello 作为导出函数,并有权使用它们,因为它们是用大写字母公开声明的。
但是,hello 变量是一个私有变量,不能导出,因为它是用小写字母声明的。通过 greeter 包导入的外部代码将不能访问 hello 变量。但是,需要记住的一个关键点,hello 变量可以由 greeter 包中的其他包文件访问,这代表了 Go 的模块化方法。
PHP 提供基于类的封装,而 Go 通过其包系统实现封装。
### 接口
接口是好的软件设计的核心,它提供了一个定义好的契约,约定了实现该接口的可以做什么,而把如何做留给实现该接口的类去实现。PHP提供了接口策略,其中接口是显式实现的。
```php
<?php
namespace App\Domain\Storage\File
interface Storage
{
public function save(File $file): void;
}
```
```php
<?php
namespace App\Domain\Storage;
class LocalStorage implements Storage
{
private array $files = [];
public function save(File $file): void
{
$this->files[] = $file;
}
}
```
上面的代码声明了一个接口,并且通过类实现了声明在该接口上的方法。
implements 关键字在具体的 LocalStorage 类和 Storage 接口之间提供了显式绑定。 Go 也提供了接口支持,尽管是隐式的。
```go
package file
type Storage interface {
Save(file File)
}
```
Go 中的接口有两层含义,首先它是一组方法的集合,其次它是一种类型。以上面的例子为例,当任何类型定义一个具有 File 参数的 Save 方法从而满足接口声明时,就会发生隐式绑定。下面的 LocalStorage 和 AwsStorage 类型就是这样,隐式地满足接口声明,而不需要正式的实现声明。
```go
package file
type LocalStorage struct {
Files []File
}
func (l LocalStorage) Save(file File) {
l.Files = append(l.Files, File)
}
type AwsStorage struct {
Client aws.Client
}
func (a AwsStorage) Save(file File) {
// Persist to external AWS storage
}
```
下面这句谚语最早出自 Go 语言创始人之一的罗伯·派克之口,在你编写接口之前,最好记住这句谚语:
> The bigger the interface, the weaker the abstraction
### Error vs Exception Handling
程序发生错误时,需要以特殊的方式通知到程序开发者。PHP 通过抛出异常来处理错误,这与其他编程语言非常相似。
代码被一个 try/catch 块包围并相应地进行处理。PHP 为开发人员提供了抛出异常的功能,而将捕获异常或再次抛出的责任留给消费类。通常常见的异常由全局异常处理程序捕获和处理。
```PHP
<?php
/**
* @param array|int[] $numbers
* @return int
* @throws \InvalidArgumentException
*/
function sum(array $numbers): int
{
if (count($numbers) === 0) {
throw new \InvalidArgumentExceptin('Array must contain one or more numbers')
}
return array_sum($numbers);
}
```
Go 语言没有提供 try/catch 来处理异常。Go 中的错误是一种类型,作为函数的返回值。按照 Go 惯例,错误一般作为函数最后一个返回值,如果没有发生错误,一般返回 nil。
```go
package count
import "errors"
func Sum(numbers []int) (int, error) {
if len(numbers) == 0 {
return 0, errors.New("numbers slice must contain one or more integer values")
}
sum := 0
for _, num := range numbers {
sum += num
}
return sum, nil
}
```
Go 中的错误处理非常冗长,因为需要对每个函数检查其错误返回值是否为 nil 值。进入Go,你会经常看到以下模式:
```go
if err != nil {
// Do something
}
```
总的来说,Go 给我们提供了一种思维方式,使程序开发者能够更深入地思考问题,错误处理系统就是一个很好的例子。该语言的设计和约定鼓励我们检查出现错误的地方,而不是抛出或有时捕获异常。
刚开始接触 Go 错误处理时,感觉非常冗长和重复。一开始可能会觉得不舒服,但是我认为正确的错误处理对于构建健壮可靠的软件是至关重要的,Go 通过设计来实现这一点。
### 依赖管理
在任何编程语言中手动管理依赖关系都非常麻烦,而且是一件非常头痛的事情。Composer 依赖项管理工具破坏了 PHP。根据它自己的定义:
> 与 Yum 或 Apt 不同,Composer 不是一个包管理器,它处理“包”或库,在每个项目的基础上管理这些“包”或库,将它们安装在项目的目录中(例如 vendor)。
Composer 已经融入到 PHP 生态系统中。它是任何 PHP 开发人员工具包中的一个成熟且重要的工具。Go中的依赖管理则是另一回事。
在这之前,Go 中的依赖管理一直是争论的焦点,直到 Go Modules 的引入,然而争论仍在继续。最初,Go 没有依赖管理工具,执行 go get 与所需依赖项的导入路径相配合,是为项目下载依赖项的唯一方法,它从存储库的 master 分支中提取最新代码。
第三方工具(如 dep )与 vendoring 等概念一起引入。到目前为止,它们还没有像 Composer 在 PHP 中获得的那样受欢迎和支持,这促使 Go 团队开发他们自己的解决方案。
Go 团队推出了 Go Modules,一个内置的依赖项管理系统,它使依赖项版本信息显式且更容易管理。依赖项在一个 go.mod 文件中声明,该文件类似于 PHP 中的 composer.json 文件。
Go1.11 和 1.12 版本已对 Go Modules 有初步支持,Go 1.13 已默认支持 Go Modules。在可预见的将来,Go Modules 被认为是 Go 项目的依赖管理解决方案。
### 测试
测试有利于我们构建良好、健壮的应用程序。测试代码可以节省宝贵的调试时间和减少不必要的麻烦,PHP 开发人员有很多第三方库可供选择,PHPUnit 是最著名的测试框架,其中包括 Codeception,Behat 和 PHPSpec。
在 PHP 中进行测试通常是通过扩展基类,通过具体实现或模拟创建依赖类并执行断言来确定我们的代码是否运行良好并按照我们的预期执行。
```php
<?php
class PersonTest extends TestCase
{
public function test_full_name_returns_a_string()
{
$person = new Person('Joe', 'Sweeny');
$this->assertEquals('Joe Sweeny', $person->fullName());
}
}
```
PHP 有第三方库可以提供测试功能。Go 提供开箱即用的测试。 Go 有一个内置的测试框架,其中包含一些首选的测试约定。
```go
package people
import "testing"
func TestFullName(t *testing.T) {
p := Person{"Joe", "Sweeny", 36}
name := p.FullName()
if name != 'Joe Sweeny' {
t.Errorf("Test failed, expected 'Joe Sweeny', got %s", name)
}
}
```
使用 go test 命令执行测试。 Go 的测试方法非常简单,但是由于该语言具有强类型和静态编译特性,因此使模拟变得更难实现且灵活性大大降低。
按照惯例,测试断言不是常态。上面测试的样式以及表驱动的测试被认为是更流行的约定。如果你对显式测试断言有更多的了解,则 tender/testify 第三方库是一款出色的工具,可为断言和模拟库提供强大的支持。
我发现 [Learn Go with Tests](https://studygolang.gitbook.io/learn-go-with-tests/) 是一个非常强大的资源,它极大地帮助我巩固了 Go 的测试方法和约定。
### 总结
我希望上面的内容能够帮助那些有 PHP 背景的人更好地了解 Go。这篇文章的目的是提供一个高层次的概述和一些对比的示例,以帮助那些想往 Go 语言方向发展的 PHP 开发人员。
文章仅仅介绍了 Go 语言一些初级知识,一些进阶知识都没有涉及,比如并发模型、内存管理等等。我强烈建议你通过以下资源来进一步学习 Go 语言:
- [Go by Example](https://gobyexample.com/)
- [A Tour of Go](https://tour.golang.org/)
Go 是一种非常简单但功能强大的语言,我非常喜欢使用它。通过设计,Go 语言变得更简单、可读性强,而不是复杂和难以遵循的抽象。与 PHP 相比,Go 是一种完全不同的语言和环境。然而,我个人发现,在 Go 中开发提高了我的整体软件开发方法,并改进了我编写 PHP 的方式。
更多评论