我為什麼從 PHP 轉到 Golang?
yamlyaml

PHP 和 Golang 的效能我想毋庸置疑是後者比較快(而且是以倍數來算),也許有的人會認為兩種不應該被放在一起比較,但 Golang 本身就是偏向 Web 開發的,所以這也是為什麼我考慮轉用 Golang 的原因,起初我的考慮有幾個:Node.js 和 Rust 還有最終被選定的 Golang;先談談 Node.js 吧。

Node.js

Node.js 的效能可以說是快上 PHP 3.5倍至6倍左右,而且撰寫的語言還是 JavaScript,蒸蚌,如此一來就不需要學習新語言了!搭配 Babel 更可以說是萬能,不過那跟「跳跳虎」一樣的 Async 邏輯還有那恐怖的 Callback Hell,有人認為前者是種優點,這點我不否認,但是對學習 PHP 的我來說太過於 "Mind Fuck",至於後者的 Callback Hell 雖然有 Promise,但是那又是另一個「Then Hell」的故事了。相較於 Golang 之下,Node.js 似乎就沒有那麼吸引我了。你確實可以用 Node.js 寫出很多東西,不過那 V8 引擎的效能仍然有限,而且要學習新的事物,不就應該是「全新」的嗎 ;)?

Rust

在拋棄改用 Node.js 之後我曾經花了一天的時間嘗試 Rust 和 Iron 框架,嗯⋯⋯Rust 太強大了,強大到讓我覺得 Rust 不應該用在這裡,這想法也許很蠢,但 Rust 讓我覺得適合更應該拿來用在系統或者是部分底層的地方,而不應該是網路服務。

Golang

go fmtgo docgo testgo get;
索引 還請先閱讀⋯

當我在撰寫這份文件的時候我會先假設你有一定的基礎,你可以先閱讀下列的手冊,他們都很不錯。


定義變數-Variables

你能夠在 PHP 裡面想建立一個變數的時候就直接建立,夭壽讚,是嗎?

PHP

$a = "foo";
$b = "bar";

Golang

蒸蚌!那麼 Golang 呢?在 Golang 中變數分為幾類:「新定義」、「預先定義」、「自動新定義」、「覆蓋」。讓我們來看看範例:

// 新定義:定義新的 a 變數為字串型別,而且值是「foo」
var a string = "foo"

// 預先定義:先定義一個新的 b 變數為字串型別但是不賦予值
var b string

// 自動新定義:讓 Golang 依照值的內容自己定義新變數的資料型態
c := "bar"

// 覆蓋:先前已經定義過 a 了,所以可以像這樣直接覆蓋其值
a = "fooooooo"

 

輸出-Echo
echo

PHP

echo "Foo"; // 輸出:Foo

$A = "Bar"
echo $A; // 輸出:Bar

$B = "Hello"
echo $B . ", world!"; // 輸出:Hello, world!

$C = [1, 2, 3];
echo var_dump($C); // 輸出:array(3) {[0]=>int(1) [1]=>int(2) [2]=>int(3)}

Golang

fmt
fmt.Println("Foo") // 輸出:Foo

A := "Bar"
fmt.Println(A) // 輸出:Bar

B := "Hello"
fmt.Printf("%s, world!", B) // 輸出:Hello, world!

C := []int{1, 2, 3}
fmt.Println(C) // 輸出:[1 2 3]

 

函式-Function

這很簡單,而且兩個語言的用法相差甚少,下面這是 PHP:

PHP

function test() {
    return "Hello, world!";
}

echo test(); // 輸出:Hello, world!

Golang

只是 Golang 稍微聒噪了一點,你必須在函式後面宣告他最後會回傳什麼資料型別。

func test() string {
    return "Hello, world!"
}

fmt.Println(test()) // 輸出:Hello, world!

多值回傳-Multiple Value

在 PHP 中你要回傳多個資料你就會用上陣列,然後將資料放入陣列裡面,像這樣。

PHP

function test() {
    return ['username' => 'YamiOdymel', 
            'time'     => 123456];
}
$data = test();

echo $data['username'], $data['time']; // 輸出:YamiOdymel 123456

Golang

然而在 Golang 中你可以不必用到一個陣列,函式可以一次回傳多個值:

func test() (string, int) {
    return "YamiOdymel", 123456
}
username, time := test()

fmt.Println(username, time) // 輸出:YamiOdymel 123456

 

匿名函式-Anonymous Function

兩個語言的撰寫方式不盡相同。

PHP

$a = function() {
    echo "Hello, world!";
};

$a(); // 輸出:Hello, world!

Golang

a := func() {
    fmt.Println("Hello, world!")
}
    
a() // 輸出:Hello, world!

 

多資料儲存型態-Stores

主要是 PHP 的陣列能做太多事情了,所以在 PHP 裡面要儲存什麼用陣列就好了。

PHP

$array  = [1, 2, 3, 4, 5];
$array2 = ['username' => 'YamiOdymel', 
           'password' => '2016 Spring'];
arrayslicemapinterface

你他媽的我到底看了三洨,首先你要知道 Golang 是個強型別語言,意思是你的陣列中只能有一種型態,什麼意思?當你決定這個陣列是用來擺放字串資料的時候,你就只能在裡面放字串。沒有數值、沒有布林值,就像你沒有女朋友一樣。

陣列-Array

一個存放固定長度的陣列。

先撇開 PHP 的「萬能陣列」不管,Golang 中的陣列既單純卻又十分腦殘,在定義一個陣列的時候,你必須給他一個長度還有其內容存放的資料型態,你的陣列內容不一定要填滿其長度,但是你的陣列內容不能超過你當初定義的長度。

PHP

$a = ["foo", "bar"];

echo $a[0]; // 輸出:foo

Golang

var a [2]string

a[0] = "foo"
a[1] = "bar"

fmt.Println(a[0]) // 輸出:foo

切片-Slice

可供「裁切」而且供自由擴展的陣列。

切片⋯⋯這聽起來也許很奇怪,但是你確實可以「切」他,讓我們先談談「切片」比起「陣列」要好在哪裡:「你不用定義其最大長度,而且你可以直接賦予值」,沒了。

PHP

$a = ["foo", "bar"];

echo $a[0]; // 輸出:foo

Golang

a := []string{"foo", "bar"}

fmt.Println(a[0]) // 輸出:foo
array_slice()slice[開始:結束]

Golang

p := []int{1, 2, 3, 4, 5, 6}

fmt.Println(p[0:1]) // 輸出:[1]
fmt.Println(p[1:1]) // 輸出:[]  (!注意這跟 PHP 不一樣!)
fmt.Println(p[1:])  // 輸出:[2, 3, 4, 5, 6]
fmt.Println(p[:1])  // 輸出:[1]
array_slice()

PHP

$p = [1, 2, 3, 4, 5, 6];

echo array_slice($p, 0, 1); // 輸出:[1]
echo array_slice($p, 1, 1); // 輸出:[2]
echo array_slice($p, 1);    // 輸出:[2, 3, 4, 5, 6]
echo array_slice($p, 0, 1); // 輸出:[1]

映照-Map

有鍵名和鍵值的陣列。

你可以把「映照」看成是一個有鍵名和鍵值的陣列,但是記住:「你需要事先定義其鍵名、鍵值的資料型態」,這仍限制你沒辦法在映照中存放多種不同型態的資料。

PHP

$data["username"] = "YamiOdymel";
$data["password"] = "2016 Spring";

echo $data["username"]; // 輸出:YamiOdymel

Golang

make()map
data := make(map[string]string)

data["username"] = "YamiOdymel"
data["password"] = "2016 Spring"

fmt.Println(data["username"]) // 輸出:YamiOdymel

接口-Interface

終於;一個可存放多種資料型態的陣列,但難以捉模(幹)。

也許你不喜歡「接口」這個詞,但用「介面」我怕會誤導大眾,所以,是的,接下來我會繼續稱其為「接口」。還記得你可以在 PHP 的關聯陣列裡面存放任何型態的資料嗎,像下面這樣?

PHP

$mixedData  = ["foobar", 123456];
$mixedData2 = ['username' => 'YamiOdymel', 
               'time'     => 123456];

Golang

interface{}
mixedData := []interface{}{"foobar", 123456}

mixedData2 := make(map[string]interface{})
mixedData2["username"] = "YamiOdymel"
mixedData2["time"]     = 123456

 

不定值-Mixed Type

有時候你也許會有個不定值的變數,在 PHP 裏你可以直接將一個變數定義成字串、數值、空值、就像你那變心的女友一樣隨時都在變。

PHP

$mixed = 123;
echo $mixed; // 輸出:123

$mixed = 'Moon, Dalan!';
echo $mixed; // 輸出:Moon, Dalan!

$mixed = ['A', 'B', 'C'];
echo $mixed; // 輸出:['A', 'B', 'C']

Golang

interface{}
var mixed interface{}

mixed = 123
fmt.Println(mixed) // 輸出:123

mixed = "Moon, Dalan!"
fmt.Println(mixed) // 輸出:Moon, Dalan!

mixed = []string{"A", "B", "C"}
fmt.Println(mixed) // 輸出:["A", "B", "C"]

 

逆向處理-Defer

當我們程式中不需要繼續使用到某個資源或是發生錯誤的時候,我們索性會將其關閉或是拋棄來節省資源開銷,例如 PHP 裡的讀取檔案:

PHP

$handle = fopen('example.txt', 'r');

if($errorA)
    errorHandlerA();

if($errorB)
    errorHandlerB();

fclose($handle); // 關閉檔案

Golang

defer
deferdeferA->B->C->DD->C->B->A
handle := file.Open("example.txt")
defer file.Close() // 關閉檔案但「推遲執行」,所有程式結束後才會執行這裡

if errorA {
    errorHandlerA()
}
if errorB {
    errorHandlerB()
}

 

跳往-Goto

這東西很邪惡,不是嗎?又不是在寫 BASIC,不過也許有時候你會在 PHP 用上呢。但是拜託,不要。

PHP

goto a;
echo 'foo';
 
a:
echo 'bar'; // 輸出:bar

Golang

goto a
fmt.Println("foo")

a:
fmt.Println("bar") // 輸出:bar

 

迴圈-Loops
forforeachwhileforfor

PHP

for($i = 0; $i < 3; $i++)
    echo $i; // 輸出:012
    
$j = 0;
for($j; $j < 5; $j++)
    echo $j; // 輸出:01234

Golang

ii := 0
for i := 0; i < 3; i++ {
    fmt.Println(i) // 輸出 012
}

j := 0
for ; j < 5 ; j++ {
    fmt.Println(j) // 輸出:01234
}

每個-Foreach

foreach()

PHP

$data = ['a', 'b', 'c'];

foreach($data as $index => $value)
    echo $index . $value . '|' ; // 輸出:0a|1b|2c|

foreach($data as $index => $value)
    echo $index . '|' ; // 輸出:0|1|2|
    
foreach($data as $value)
    echo $value . '|' ; // 輸出:a|b|c|

Golang

for()rangeforeach
data := []string{"a", "b", "c"}

for index, value := range data {
    fmt.Printf("%d%s|", index, value)  // 輸出:0a|1b|2c|
}

for index := range data {
    fmt.Printf("%d|", index)  // 輸出:0|1|2|
}

for _, value := range data {
    fmt.Printf("%s|", value)  // 輸出:a|b|c|
}

重複-While

while(條件)條件false

PHP

$i = 0;

while( $i < 3 ) {
    $i++;
    echo $i; // 輸出:123
}

while(true)
    echo "WOW" // 輸出:WOWWOWWOWWOWWOW...

Golang

forfor;for
i := 0

for i < 3 {
    i++
    fmt.Println(i) // 輸出:123
}

for {
    fmt.Println("WOW") // 輸出:WOWWOWWOWWOWWOW...
}

做 .. 重複-Do While

do .. while()

PHP

$i = 0;

do {
    $i++;
    echo $i; // 輸出:123
} while($i < 3);

Golang

for
i := 0

for {
    i++
    fmt.Println(i) // 輸出:123
    
    // 注意這個條件式和 PHP 有所不同
    if i > 2 {
        break
    }
}

Golang

goto
i := 0

LOOP:
    i++
    fmt.Println(i) // 輸出:123
    
    if i < 3 {
        goto LOOP
    }

 

日期-Date
date()

PHP

echo date("Y-m-d H:i:s"); // 輸出:2016-07-13 12:59:59

Golang

Y-m-d1231
fmt.Println(time.Now().Format("2006-2-1 03:04:00"))          // 輸出:2016-07-13 12:59:59
fmt.Println(time.Now().Format("Mon, Jan 2, 2006 at 3:04pm")) // 輸出: Mon, Jul 13, 2016 at 12:59pm

 

切割字串-Split
explode()die()

PHP

$data  = 'a, b, c, d';
$array = explode(', ', $data);

Golang

簡單的就讓一個字串給「爆炸」了,那麼 Golang 呢?

data  := "a, b, c, d"
array := strings.Split(data, ", ")
strings

 

關聯陣列-Associative Array

這真的是很常用到的功能,就像物件一樣有著鍵名和鍵值,在 PHP 裡面你很簡單的就能靠陣列(Array)辦到。

PHP

$data = ['username' => 'YamiOdymel',
         'password' => '2016 Spring'];
         
echo $data["username"]; // 輸出:YamiOdymel

Golang

map
data := map[string]string{
           "username": "YamiOdymel", 
           "password": "2016 Spring"}

fmt.Println(data["username"]) // 輸出:YamiOdymel

 

是否存在-Isset
isset()

PHP

// 如果 $data['username'] 存在
if(isset($data['username'])) {
    $username = $data['username'];
}

Golang

map
username, exists := data["username"]

if !exists {
    fmt.Printf("你要找的資料不存在。")
}

 

指針-Pointer
A = 1; B = A;BABA

指針比起複製一個變數,他會建立一個指向到某個變數的記憶體位置,這也就是為什麼你改變指針,實際上是在改變某個變數。

PHP

function zero(&$number) { // & 即是指針
    $number = 0;
}

$A = 5;
zero($A);

echo $A; // 輸出:0

Golang

*&
func zero(number *int) {
    number = 0
}

func main() {
    A := 5;
    zero(&A)

    fmt.Printf("%d", A) // 輸出:0
}

 

錯誤處理-Error Exception

有些時候你會回傳一個陣列,這個陣列裡面可能有資料還有錯誤代號,而你會用條件式判斷錯誤代號是否非空值。

PHP

function foo($number) {
    if($number !== 1)
        return ['number' => -1, 
                'error'  => '$number is not 1'];
    
    return ['number' => $number, 
            'error'  => null];
}

$bar = foo(0);

if($bar['error'])
    echo $bar['number'], $bar['error']; // 輸出:-1
                                        //      $number is not 1

Golang

errorerrors
try .. catchtry
import "errors"

func foo(number int) (int, error) {
    if number != 1 {
        return -1, errors.New("$number is not 1")
    }
    return number, nil
}

if bar, err := foo(0); err != nil {
    fmt.Println(bar, err) // 輸出:-1
                          //      $number is not 1
}
ifif

拋出和捕捉異常-Try & Catch

try .. catchtry

PHP

function foo($number) {
    if($number < 10)
        throw new Exception('$number is less than 10');
    else if($number > 10)
        throw new Exception('$number is greater than 10');
}

try {
    foo(9);
} catch(Exception $e) {
    echo $e->getMessage(); // 輸出:$number is less than 10
}

try {
    foo(11);
} catch(Exception $e) {
    echo $e->getMessage(); // 輸出:$number is greater than 10
}

Golang

try .. catchpanic()recover()defer
panic()throwexit()panic()deferdeferpanic()
deferpanic()deferdeferrecover()
recover()catchdeferrecover()panic()
// 建立一個模仿 try&catch 的函式供稍後使用
func try(fn func(), handler func(interface{})) {
    // 這不會馬上被執行,但當 panic 被執行就會結束程式,結束程式就必定會呼叫 defer
    defer func() { 
        // 透過 recover 來從 panic 狀態中恢復,並呼叫捕捉函式
        if err := recover(); err != nil {
            handler(err)
        }
    }()
    // 執行可能帶有 panic 的程式
    fn()
}

func foo(number int) {
    if number < 10 {
        panic("number is less than 10")
    }
    if number > 10 {
        panic("number is greater than 10")
    }
}

func main() {
    try(func() {
        foo(9)
    }, func(e interface{}) {
        fmt.Println(e) // 輸出:number is less than 10
    })
    
    try(func() {
        foo(11)
    }, func(e interface{}) {
        fmt.Println(e) // 輸出:number is greater than 10
    })
}

 

套件/匯入/匯出-Package / Import / Export
require()include()

PHP

// a.php
<?php
    $foo = "bar";
?>
// index.php
<?php
    include "a.php";
    
    echo $foo; // 輸出:bar
?>
Include Hell

Golang

// a.go
package main

var foo string = "bar"
// main.go
package main

import "fmt"

func main() {
    fmt.Println(foo) // 輸出:bar
}
main.gofmta.go

蛤???殺小??????」你彷彿回到了幾秒鐘前的自己。

main.gofoomain

套件-Package

.go
package main
main

PHP

// a.php
<?php
    function foo() {
        // ...
    }
?>
// index.php
<?php
    include "a.php";
    
    foo();
?>

Golang

接著是 Golang;注意!你不需要引用任何檔案,因為下列兩個檔案同屬一個套件。

// a.go
package main

func foo() {
    // ...
}
// main.go
package main

func main() {
    foo()
}
include()require()

匯入-Import

在 Golang 中沒有引用單獨檔案的方式,你必須匯入一整個套件,而且你要記住:「一定你匯入了,你就一定要使用它」,像下面這樣。

package main

import (
    "fmt"                           // 引用底層套件
    "time"                          // 這也是底層套件
    "github.com/yamiodymel/teameow" // 來自 Github 的 "teameow" 套件
)

func main() {
    // 然後像下面這樣使用你剛匯入的套件
    fmt.XXX()
    time.XXX()
    teameow.XXX()
}
main()_
import (
    _ "fmt"
)

如果你的套件出現了名稱衝突,你可以在套件來源前面給他一個新的名稱。

import (
    "github.com/karisu/teameow"
    neko "github.com/yamiodymel/teameow"
)

func main() {
    teameow.XXX()
    neko.XXX()
}

匯出-Export

現在你知道可以匯入套件了,那麼什麼是「匯出」?同個套件內的函式還有共享變數確實可以直接用,但那並不表示可以給其他套件使用,其方法取決於函式/變數的「開頭大小寫」

是的。Golang 依照一個函式/變數的開頭大小寫決定這個東西是否可供「匯出」

// a.go
package hello

// 注意:這裡的 Foo 的開頭字母是大寫!
var Foo string = "bar"

// 注意:這個 World 函式的開頭字母是大寫!
func World() {
    // ...
}
// b.go
package test

import (
    "hello"
    "fmt"
)

func main() {
    fmt.Println(hello.Foo) // 輸出:bar
    
    hello.World()
}

這用在區別函式的時候格外有用,因為小寫開頭的任何事物都是不供匯出的,反之,大寫開頭的任何事物都是用來匯出供其他套件使用的。

publicprivateprotected

 

類別-Class

在 Golang 中沒有類別,但有所謂的「建構體(Struct)」和「接口(Interface)」,這就能夠滿足幾乎所有的需求了,這也是為什麼我認為 Golang 很簡潔卻又很強大的原因。

讓我們先用 PHP 建立一個類別,然後看看 Golang 怎麼解決這個問題。

PHP

class Foobar {
    public $a = "hello, world";
    
    public function test() {
        echo $this->a;
    }
}

$b = new Foobar();
$b->test(); // 輸出:hello, world!

Golang

publicprivateprotected
// 先定義一個 Foobar 建構體,然後有個叫做 a 的字串成員
type Foobar struct {
    a string
}

// 定義一個屬於 Foobar 的 test 方法
func (f *Foobar) test () {
    // 接收來自 Foobar 的 a(等同於 PHP 的 `$this->a`)
    fmt.Println(f.a)
}

b := &Foobar{a: "hello, world!"}
b.test() // 輸出:hello, world!

建構子-Constructor

new__construct()

PHP

class Test{
    public $a;
    
    function __construct() {
        $this->a = "foobar";
    }
    
    function show() {
        echo $this->a;
    }
}

$b = new Test();
$b->show(); // 輸出:foobar

Golang

但是在 Golang 裡因為沒有類別,也就沒有建構子,不巧的是建構體本身也不帶有建構子的特性,這個時候你只能自己在外部建立一個建構用函式。

type Test struct {
    a string
}

func (t *Test) show() {
    fmt.Println(t.a)
}

// 用來建構 Test 的假建構子
func newTest() (test *Test) {
    test = &Test{a: "foobar"}
    
    // 這裡會回傳一個型態是 *Test 建構體的 test 變數
    return
}

b := newTest()
b.show() // 輸出:foobar

嵌入-Embed

讓我們假設你有兩個類別,你會把其中一個類別傳入到另一個類別裡面使用,廢話不多說!先上個 PHP 範例(為了簡短篇幅我省去了換行)。

PHP

class Foo {
    public $msg = "Hello, world!";
}

class Bar {
    public $foo;
    
    function __construct($foo){ $this->foo = $foo;    }
    function show()           { echo $this->foo->msg; }
}

$a = new Foo();
$b = new Bar($a);
$b->show(); // 輸出:Hello, world!

Golang

在 Golang 中你也有相同的用法,但是請記得:「任何東西都是在「類別」外完成建構的」。

type Foo struct {
    msg string
}

type Bar struct {
    *Foo
}

func (b *Bar) show() {
    // Foo 中的 msg 會直接暴露在 Bar 底下
    // 所以你可以直接使用 b.msg
    fmt.Println(b.msg)
}

a := &Foo{msg: "Hello, world!"}
b := &Bar{a}
b.show() // 輸出 Hello, world!

遮蔽-Shadowing

在 PHP 中沒有相關的範例,這部分會以剛才「嵌入」章節中的 Golang 範例作為解說對象。

FooBarFooBar

這個時候被嵌入的成員就會被「遮蔽」,下面是個實際範例,還有你如何解決遮蔽問題:

type Foo struct {
    msg string
}

type Bar struct {
    *Foo
    msg string // 遮蔽了 Foo 的 msg
}

a := &Foo{msg: "Hello, world!"}
b := &Bar{Foo: a, msg: "Moon, Dalan!"}

fmt.Println(b.msg)     // 輸出:Moon, Dalan!
fmt.Println(b.Foo.msg) // 輸出:Hello, world!

多形-Polymorphism

雖然都是呼叫同一個函式,但是這個函式可以針對不同的資料來源做出不同的舉動,這就是多形。你也能夠把這看作是:「訊息的意義由接收者定義,而不是傳送者」。

目前 PHP 中沒有真正的「多形」,不過你仍可以做出同樣的東西。

PHP

class Foo{ public $msg = "hello";  }
class Bar{ public $msg = "world!"; }

class Handler {
    public function process($class) {
        switch(get_class($class)) {
            // 依照不同的資料類型做出不同的舉動
            case 'Foo':
                echo '處理 Foo | ' . $class->msg . ', world!';
                break;
                
            case 'Bar':
                echo '處理 Bar | ' . 'hello, ' . $class->msg;
                break;
        }
    }
}

$foo = new Foo();
$bar = new Bar();
$handler = new Handler();

// 雖然都是同個函式,但是可以處理不同資料
$handler->process($foo); // 輸出:處理 Foo | hello, world!
$handler->process($bar); // 輸出:處理 Bar | hello, world!

Golang

interface
type Foo struct {
    msg string
}

type Bar struct {
    msg string
}

// 透過 Handler 實作 process
type Handler interface {
    process()
}

// 處理 Foo 資料的 process
func (f Foo) process() {
    fmt.Printf("處理 Foo | %s, world!", f.msg)
}

// 處理 Bar 資料的 process
func (b Bar) process() {
    fmt.Printf("處理 Bar | hello, %s", b.msg)
}

foo := Foo{msg: "hello"}
bar := Bar{msg: "world!"}

// 雖然都是同個函式,但是可以處理不同資料
Handler.process(foo) // 輸出:處理 Foo | hello, world!
Handler.process(bar) // 輸出:處理 Bar | hello, world!