如何利用不變性來增強你的Golang 應用程式的可讀性和穩定性
不變性的概念非常簡單. 創建物件(或結構體) 後, 將永遠無法更改它. 這是一成不變的. 儘管這個概念看起來很簡單, 但使用它或從中受益並不那麼容易.
正如計算機科學(和生活) 中的大多數事物一樣, 有許多種方法可以達到相同的結果, 就不變性而言, 兩者沒有什麼不同. 您應該把它看做是工具包中的一個工具, 並使用在適用的問題場景上. 關於不變性的一個非常好的用例是在您進行並發編程時. Golang 在設計時就考慮了並發性, 因此在go 中使用並發非常普遍.
無論您使用哪個範例都可以透過以下方法在Golang 中使用一些不變性概念來使程式碼更具可讀性和穩定性.
這與封裝類似. 使用非導出字段創建結構, 僅導出作用的函數. 由於您只對那些結構的行為感興趣, 因此該技術對介面非常有用. 這項技術的另一個很好的補充是將創建函數(或構造函數) 添加並導出到您的結構中. 這樣您可以確保該結構的狀態始終有效. 始終保持有效狀態可以使程式碼更加可靠, 因為您不必繼續處理要對該結構進行的每個操作的無效狀態. 下面是一個非常基本的範例:
package amounts import "errors" type Amount struct { value int } func NewAmount(value int) (Amount, error) { if value < 0 { return Amount{}, errors.New("Invalid amount") } return Amount{value: value}, nil } func (a Amount) GetValue() int { return a.value }
在此程式包中, 我們定義了Amount
類型, 具有未匯出的欄位value
, 建構子NewAmount
以及GetValue
方法用於Amount
類型. 一旦NewAmount
函數創建了Amount
結構, 就無法更改它. 因此它從包的外部來說是不可變的(儘管在go 2 中有更改此內容的建議, 但go 1 中沒有創建不變結構的方法). 此外沒有處於無效狀態(在這種情況下為負數) 的Amount
類型的變數, 因為創建它們的唯一方法已經對此進行了驗證. 我們可以從另一個套件中呼叫它:
a, err := amounts.NewAmount(10) *// 处理错误 *log.Println(a.GetValue())
#最基本的概念是在建立一個物件(或結構體)後,再也不去改變它。但是我們經常在實體狀態很重要的應用上工作。不過,程式中實體狀態和實體內部表示法是不同的。在使用不變性時,我們仍然可以給予實體多個狀態。這意味著已建立的結構體不會改變,但是它的副本會改變。這並不意味著我們需要手動實現複製結構體中每個欄位的功能。
相反地,當呼叫函數時我們可以依賴 Go 語言複製值的本機行為。對於任一會改變實體狀態的操作,我們可以建立一個用來接收結構體作為參數(或作為函數接收器)的函數,在執行完畢之後傳回改變後的版本。這是一項非常強大的技術,因為你能夠改變副本上的任何內容,而無需更改函數呼叫者作為參數傳遞的變數。這意味著沒有副作用和可預測的行為。如果相同的結構體被傳遞給並發函數,每個結構體都會接收到它的副本,而不是指向它的指標。
當你在使用切片功能時,你會看到此行為應用於[append](https://golang.org/pkg/builtin/#append)
函數
回到我們的例子中,讓我們實作Account
類型,它包含了Amount
類型的balance
欄位。同時,我們加入 Deposit
和 Withdraw
方法來改變 Account
實體的狀態。
package accounts import ( "errors" "my-package/amounts" ) type Account struct { balance amounts.Amount } func NewEmptyAccount() Account { amount, _ := amounts.NewAmount(0) return NewAccount(amount) } func NewAccount(amount amounts.Amount) Account { return Account{balance: amount} } func (acc Account) Deposit(amount amounts.Amount) Account { newAmount, _ := amounts.NewAmount(acc.balance.GetValue() + amount.GetValue()) acc.balance = newAmount return acc } func (acc Account) Withdraw(amount amounts.Amount) (Account, error) { newAmount, err := amounts.NewAmount(acc.balance.GetValue() - amount.GetValue()) if err != nil { return acc, errors.New("Insuficient funds") } acc.balance = newAmount return acc, nil }
如果你檢查我們創建的方法,他們會覺得我們事實上改變了作為函數接收器的 Account
結構的狀態。由於我們沒有使用指針,情況並非如此,由於結構體的副本作為這些函數的接收器來傳遞,我們將更改只在函數作用域內有效的副本,然後返回它。這是在另一個套件中呼叫它的範例:
a, err := amounts.NewAmount(10) acc := accounts.NewEmptyAccount() acc2 := acc.Deposit(a) log.Println(acc.GetBalance()) log.Println(acc2.GetBalance())
命令列上的結果會是這樣的:
2020/06/03 22:22:40 {0} 2020/06/03 22:22:40 {10}
如你所見,儘管透過變數acc
呼叫了Deposit
方法,但實際上變數並沒有改變,它傳回了新的 Account
副本(指派給acc2
),其包含了變更後的字段。
使用指针具有优于复制值的优点,特别是如果您的结构很大时,在复制时可能会导致性能问题,但是您应始终问自己是否值得,不要尝试过早地优化代码。尤其是在使用并发时。您可能会在一些糟糕的情况下结束。
不变性不仅可以应用于结构,还可以应用于函数。如果我们用相同的参数两次执行相同的函数,我们应该收到相同的结果,对吗?好吧,如果我们依赖于外部状态或全局变量,则可能并非总是如此。最好避免这种情况。有几种方法可以实现这一目标。
如果您在函数内部使用共享的全局变量,请考虑将该值作为参数传递,而不是直接在函数内部使用。 那会使您的函数更可预测,也更易于测试。整个代码的可读性也会更容易,其他人也将会了解到值可能会影响函数行为,因为它是一个参数,而这就是参数的用途。 这里有一个例子:
package main import ( "fmt" "time" ) var rand int = 0 func main() { rand = time.Now().Second() + 1 fmt.Println(sum(1, 2)) } func sum(a, b int) int { return a + b + rand }
这个函数 sum
使用全局变量作为自己计算的一部分。 从函数签名来看这不是很清楚。 更好的方法是将rand变量作为参数传递。 因此该函数看起来应该像这样:
func sum(a, b, rand **int**) **int** { return a + b + rand }
推荐教程:《Go教程》
以上是詳解 Go 中的不可變類型的詳細內容。更多資訊請關注PHP中文網其他相關文章!