如何避免用動態語言的思維寫 Go 代碼?

許多從PHP工程師轉型用Go語言進行開發的人,其代碼中還是能被發現很多PHP的影子。if語句後面非要帶括號這種問題就不說了,gofmt就會強行把你掰過來。最大的問題還是因爲以前用慣了PHP的數組,到寫Go代碼時還是不習慣先定義類型後使用這種習慣。還有就是以前寫PHP的時候可能沒養成使用異常的習慣,在返回值里約定特殊值來代表錯誤。今天我們就來講一講,寫以 PHP 等動態語言的程序員可能在開始用Go寫代碼時都容易犯的一些錯誤。

Go 編程的注意事項及建議

接下來我們會說幾個PHP程序員在剛開始用Go寫程序時幾個需要改變的編碼習慣和要注意的地方。

儘量使用結構體切片代替字典

我們有的新同學特別愛使用Go裏面的Map,有的時候還是切片裏邊套Map,比如我看一開始有的同學把一些配置信息放在map[string]string類型的Map裏,多個的話再把Map放進切片裏,比如這樣。

var configMap = []map[string]string{
 {
  "stockNum""100",
  "name":     "芒果TV周卡",
  "type":     "virtual",
    },
}

後面程序使用的時候再去用鍵去取值,這麼做程序當然能實現,但你會發現Go裏面因爲是強類型,你在用上面字典裏面的數值時還得對他們做類型轉換。很多同學馬上會說,那我把Map的類型換成map[string]interface{},我只能說你試試,看你用的時候Go讓不讓你做類型斷言。

這其實是涉及一個思維的轉變,那麼在像Go這樣的強類型語言裏針對這種情況該怎麼辦呢?這就需要讓我們養成先定義結構體類型後使用的習慣了,比如像上面的情況我就可以先定義一個類型。

type Product struct {
 StockNum  int64
 Name      string
 Type      string
}

var configs = []*Product {
 {
  StockNum: 100,
  Name: "芒果TV周卡",
  Type: "virtual",
 },
  ......
}

這麼做就能避免像上面那樣使用StockNum前還得把它轉成整型的問題了,而且編輯器還能做類型提示,不需要你刻意記得 Map 裏的鍵,還能避免你一時疏忽把鍵拼錯導致 BUG 的尷尬。

除了上面說的還有人喜歡在返回值裏返回Map,這種寫法除了會導致上面說的那樣問題,讓別人使用起來也特別不方便。比如我要用你的方法我還得進去看看你的代碼裏這個Map到底有哪些鍵。

所以我們寫Go代碼時,其實Map的使用率要比在PHP裏使用數組低很多,很多時候都是用結構體以及結構體切片的,對於那種 key 爲數據 ID,值爲數據Map的這種映射,也是改成Key爲數據ID,值爲數據自己定義的類型纔對。比如下面這個Map類型的變量,它的Key是產品的 ID,值的類型是我們上面定義的Product結構體

var productMap = map[int64]*Product {
 123:  {
  StockNum: 100,
  Name: "芒果TV周卡",
  Type: "virtual",
 },
}

針對這部分說的這個問題我覺得記住:"根據數據先定類型再使用" 這個原則就行了。

說完這個在代碼裏出現率最高的問題後,下面我們再說幾個寫Go代碼時的要注意的細節。

零值陷阱

未進行初始化的變量默認值爲其類型的零值,需要注意的是slicemapchan*T類型對應的零值是nil

這些類型的變量在未初始化前是無法在程序裏直接使用的,有些情況下會導致運行時錯誤。

常見的兩種運行時錯誤是:

第一個錯誤是因爲對一個未初始化的map進行賦值導致的,所以使用map類型的變量前要記得用make函數對變量進行初始化,與map類似的切片在使用append函數 向nil slice追加新元素就可以,原因是append函數會生成新的切片,在底層爲切片分配了底層數組。

第二個錯誤是對nil指針進行了解引用導致的,指針的零值nil*T{}並不相等。所以指針類型的變量在使用前要注意使用new函數進行初始化。

還有就是前端同學們非常不喜歡接口返回值的字段有數據的時候是個列表,沒數據的時候是Null,這也是切片未初始化導致的,如果數據庫裏沒查到數據,那麼在代碼邏輯裏就執行不到給切片append數據的循環裏,所以就會出現這個問題。這是一個保持接口字段類型一致性的一個很重要的細節。

使用 error 返回函數錯誤

在使用PHP時,函數的錯誤是通過拋出異常,甚至是通過返回0false之類的值來表示函數遇到的錯誤(這種,即使寫PHP也不推薦這種做法)

比如好的寫法,可這樣寫:

public function updateUserFavorites(User $user$favoriteData)
{
    try {
        // database execution
    ......
    } catch (QueryException $queryException) {
        throw new UserManageException(func_get_args()'Error Message''501' , $queryException);
    }

    return true;
}

但很多的人會這麼寫:

public function updateUserFavorites(User $user$favoriteData)
{
    // database execution
  if ($conn.AffectedRows <= 0) {
        return false
    }

    return true;
}

Go語言裏雖然沒有異常機制,但是可以讓函數返回error明確遇到的錯誤。所以除非確定函數不需要返回error,多數情況下我們的函數都是需要返回error的,所以在定義函數時要明確,返回的數據和error的區別,兩種返回值的職責範圍不一樣。要通過函數返回的error是否爲空,而不是返回數據是0或者false之類的值判斷函數是否執行成功。

謹慎使用map[string]interface{}做參數

寫過PHP的同學都知道,PHP裏的數組近乎萬能,可以用來當列表、字典,而且當字典用時還能保證字典key的遍歷順序,這點是很多語言的字典類型辦不到的事情。

很多剛從PHP轉到用Go開發的同學還是帶着在PHP裏使用數組參數的習慣,那麼在Go語言裏,最像PHP數組的可能就是map[string]interface{}了。

這種還是典型的動態語言編程的思維,在使用Go的時候,針對比較複雜的代表一類事物的參數,我們也是應該先定義結構體,然後使用結構體指針或者結構體指針切片作爲參數。儘量不使用map[string]interface{}這種類型的參數,IDE也沒法幫助提示這些參數的內部結構,這讓其他人使用這個代碼時就會很苦惱,還得先看看函數實現裏具體用到了字典的哪些鍵。比如下面這兩個函數的對比:

type UserInput struct{
  Name     string
  Age      int32
  Password string
}
func AuthenticateUser(input *UserInput) error {
    findUser(input.Name, input.Password)
    ...
}

func DummyAuthenticateUser(input map[string]interface{}) error {
    findUser(input["name"], input["password"])
    ...
}

一般在業務級別的程序開發裏,我們要傳遞存儲在數據表裏的額外信息的時候纔會使用到map[string]interface{}類型的參數。寫表前把這部分數據編碼成JSON格式再寫入,當然這個主要看使用場景,凡事沒有絕對,這裏只是強調一些在編碼習慣上的問題。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xeCL3gwOGzWp7MCazLl8zA