0

0

详解 Go 中的不可变类型

Guanhui

Guanhui

发布时间:2020-06-15 18:01:56

|

4754人浏览过

|

来源于learnku

转载

详解 Go 中的不可变类型

Golang 中的不变性

如何利用不变性来增强你的 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 字段。同时,我们添加 DepositWithdraw 方法来改变 Account 实体的状态。

亿众购物系统
亿众购物系统

一套设计完善、高效的web商城解决方案,独有SQL注入防范、对非法操作者锁定IP及记录功能,完整详细的记录了非法操作情况,管理员可以随时查看网站安全日志以及解除系统自动锁定的IP等前台简介:  1)系统为会员制购物,无限会员级别。  2)会员自动升级、相应级别所享有的折扣不同。  3)产品可在缺货时自动隐藏。  4)自动统计所有分类中商品数量,并在商品分类后面显示。  5)邮件列表功能,可在线订阅

下载
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教程

相关专题

更多
golang如何定义变量
golang如何定义变量

golang定义变量的方法:1、声明变量并赋予初始值“var age int =值”;2、声明变量但不赋初始值“var age int”;3、使用短变量声明“age :=值”等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

178

2024.02.23

golang有哪些数据转换方法
golang有哪些数据转换方法

golang数据转换方法:1、类型转换操作符;2、类型断言;3、字符串和数字之间的转换;4、JSON序列化和反序列化;5、使用标准库进行数据转换;6、使用第三方库进行数据转换;7、自定义数据转换函数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

226

2024.02.23

golang常用库有哪些
golang常用库有哪些

golang常用库有:1、标准库;2、字符串处理库;3、网络库;4、加密库;5、压缩库;6、xml和json解析库;7、日期和时间库;8、数据库操作库;9、文件操作库;10、图像处理库。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

337

2024.02.23

golang和python的区别是什么
golang和python的区别是什么

golang和python的区别是:1、golang是一种编译型语言,而python是一种解释型语言;2、golang天生支持并发编程,而python对并发与并行的支持相对较弱等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

208

2024.03.05

golang是免费的吗
golang是免费的吗

golang是免费的。golang是google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的开源编程语言,采用bsd开源协议。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

391

2024.05.21

golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

196

2025.06.09

golang相关判断方法
golang相关判断方法

本专题整合了golang相关判断方法,想了解更详细的相关内容,请阅读下面的文章。

191

2025.06.10

golang数组使用方法
golang数组使用方法

本专题整合了golang数组用法,想了解更多的相关内容,请阅读专题下面的文章。

192

2025.06.17

java数据库连接教程大全
java数据库连接教程大全

本专题整合了java数据库连接相关教程,阅读专题下面的文章了解更多详细内容。

20

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
golang socket 编程
golang socket 编程

共2课时 | 0.1万人学习

nginx浅谈
nginx浅谈

共15课时 | 0.8万人学习

golang和swoole核心底层分析
golang和swoole核心底层分析

共3课时 | 0.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号