目录
Golang 中的不变性" >Golang 中的不变性
仅导出结构体的功能, 而不导出其字段" >仅导出结构体的功能, 而不导出其字段
在函数中使用值拷贝替代指针" >在函数中使用值拷贝替代指针
减少全局或外部状态中的依赖性" >减少全局或外部状态中的依赖性
首页 后端开发 Golang 详解 Go 中的不可变类型

详解 Go 中的不可变类型

Jun 15, 2020 pm 06:01 PM
go golang

详解 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 实体的状态。

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中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
4 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何使用 Golang 安全地读取和写入文件? 如何使用 Golang 安全地读取和写入文件? Jun 06, 2024 pm 05:14 PM

在Go中安全地读取和写入文件至关重要。指南包括:检查文件权限使用defer关闭文件验证文件路径使用上下文超时遵循这些准则可确保数据的安全性和应用程序的健壮性。

如何为 Golang 数据库连接配置连接池? 如何为 Golang 数据库连接配置连接池? Jun 06, 2024 am 11:21 AM

如何为Go数据库连接配置连接池?使用database/sql包中的DB类型创建数据库连接;设置MaxOpenConns以控制最大并发连接数;设置MaxIdleConns以设定最大空闲连接数;设置ConnMaxLifetime以控制连接的最大生命周期。

golang框架的优缺点比较 golang框架的优缺点比较 Jun 05, 2024 pm 09:32 PM

Go框架凭借高性能和并发性优势脱颖而出,但也存在一些缺点,如相对较新、开发者生态系统较小、缺少某些功能。此外,快速变化和学习曲线可能因框架而异。Gin框架以其高效路由、内置JSON支持和强大的错误处理而成为构建RESTfulAPI的热门选择。

如何在 Golang 单元测试中使用 gomega 进行断言? 如何在 Golang 单元测试中使用 gomega 进行断言? Jun 05, 2024 pm 10:48 PM

如何在Golang单元测试中使用Gomega进行断言在Golang单元测试中,Gomega是一个流行且功能强大的断言库,它提供了丰富的断言方法,使开发人员可以轻松验证测试结果。安装Gomegagoget-ugithub.com/onsi/gomega使用Gomega进行断言以下是使用Gomega进行断言的一些常用示例:1.相等断言import"github.com/onsi/gomega"funcTest_MyFunction(t*testing.T){

Golang框架与Go框架:内部架构与外部特性对比 Golang框架与Go框架:内部架构与外部特性对比 Jun 06, 2024 pm 12:37 PM

GoLang框架与Go框架的区别体现在内部架构和外部特性上。GoLang框架基于Go标准库,扩展其功能,而Go框架由独立库组成,实现特定目的。GoLang框架更灵活,Go框架更容易上手。GoLang框架在性能上稍有优势,Go框架的可扩展性更高。案例:gin-gonic(Go框架)用于构建RESTAPI,而Echo(GoLang框架)用于构建Web应用程序。

Golang 框架中的错误处理最佳实践有哪些? Golang 框架中的错误处理最佳实践有哪些? Jun 05, 2024 pm 10:39 PM

最佳实践:使用明确定义的错误类型(errors包)创建自定义错误提供更多详细信息适当记录错误正确传播错误,避免隐藏或抑制根据需要包装错误以添加上下文

如何在 Golang 中将 JSON 数据保存到数据库中? 如何在 Golang 中将 JSON 数据保存到数据库中? Jun 06, 2024 am 11:24 AM

可以通过使用gjson库或json.Unmarshal函数将JSON数据保存到MySQL数据库中。gjson库提供了方便的方法来解析JSON字段,而json.Unmarshal函数需要一个目标类型指针来解组JSON数据。这两种方法都需要准备SQL语句和执行插入操作来将数据持久化到数据库中。

如何解决golang框架中常见的安全问题? 如何解决golang框架中常见的安全问题? Jun 05, 2024 pm 10:38 PM

如何在Go框架中解决常见的安全问题随着Go框架在Web开发中的广泛采用,确保其安全至关重要。以下是解决常见安全问题的实用指南,附带示例代码:1.SQL注入使用预编译语句或参数化查询来防止SQL注入攻击。例如:constquery="SELECT*FROMusersWHEREusername=?"stmt,err:=db.Prepare(query)iferr!=nil{//Handleerror}err=stmt.QueryR

See all articles