>  기사  >  백엔드 개발  >  Go의 불변 유형에 대한 자세한 설명

Go의 불변 유형에 대한 자세한 설명

Guanhui
Guanhui앞으로
2020-06-15 18:01:564296검색

Go의 불변 유형에 대한 자세한 설명

Golang의 불변성

불변성을 사용하여 Golang 애플리케이션의 가독성과 안정성을 높이는 방법

불변성의 개념은 매우 간단합니다. 객체(또는 구조)를 만든 후에는 불가능합니다. 개념은 단순해 보이지만 이를 사용하거나 활용하기는 쉽지 않습니다.

컴퓨터 과학(및 생활)의 대부분과 마찬가지로 이를 수행하는 방법은 많습니다. 동일한 결과를 얻을 수 있으며 불변성 측면에서는 차이가 없습니다. 이를 툴킷의 도구로 생각하고 적용 가능한 문제 시나리오에 사용해야 합니다. 불변성에 대한 매우 좋은 사용 사례는 동시 프로그래밍을 수행할 때입니다.

어떤 패러다임을 사용하든 다음 방법을 사용하여 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, 생성자 NewAmountAmount 유형에 대한 GetValue 메서드가 있는 유형입니다. 함수는 Amount 구조를 생성하므로 변경할 수 없습니다. 따라서 패키지 외부에서 변경할 수 없습니다(go 2에서는 이를 변경하라는 제안이 있지만 변경 불가능을 생성할 수 있는 방법은 없습니다). go 1)의 구조. 또한 잘못된 상태(이 경우 음수)인 Amount 유형의 변수가 없습니다. 이를 생성하는 유일한 방법이 확인되었기 때문입니다. 다른 패키지에서 가져왔습니다:

a, err := amounts.NewAmount(10)
*// 处理错误
*log.Println(a.GetValue())

함수에서 포인터 대신 값 복사본 사용Amount 类型, 具有未导出的字段 value, 构造函数 NewAmount以及 GetValue 方法用于 Amount类型. 一旦 NewAmount 函数创建了 Amount 结构, 就无法更改它. 因此它从包的外部来说是不可变的 (尽管在 go 2 中有 更改此内容的建议, 但 go 1 中没有创建不变结构的方法). 此外没有处于无效状态 (在这种情况下为负数) 的 Amount 类型的变量, 因为创建它们的唯一方法已经对此进行了验证. 我们可以从另一个包中调用它:

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
}

在函数中使用值拷贝替代指针

最基本的概念是在创建一个对象(或者结构体)后,再也不去改变它。但是我们经常在实体状态很重要的应用上工作。不过,程序中实体状态和实体内部表示是不同的。在使用不变性时,我们仍然可以给实体赋予多个状态。这意味着已创建的结构体不会改变,但是它的副本会改变。这并不意味着我们需要手动实现复制结构体中每个字段的功能。

相反地,当调用函数时我们可以依赖 Go 语言复制值的本机行为。对于任意一个会改变实体状态的操作,我们可以创建一个用来接收结构体作为参数(或者作为函数接收器)的函数,在执行完毕之后返回改变后的版本。这是一项非常强大的技术,因为你能够改变副本上的任何内容,而无需更改函数调用者作为参数传递的变量。这意味着没有副作用和可预测的行为。如果相同的结构体被传递给并发函数,每个结构体都会接收到它的副本,而不是指向它的指针。

当你在使用切片功能时,你会看到此行为应用于 [append](https://golang.org/pkg/builtin/#append) 函数

回到我们的例子中,让我们实现 Account 类型,它包含了
Amount 类型的 balance 字段。同时,我们添加 DepositWithdraw 方法来改变 Account 实体的状态。

a, err := amounts.NewAmount(10)
acc := accounts.NewEmptyAccount()
acc2 := acc.Deposit(a)
log.Println(acc.GetBalance())
log.Println(acc2.GetBalance())

如果你检查我们创建的方法,他们会觉得我们事实上改变了作为函数接收器的 Account 结构的状态。由于我们没有使用指针,情况并非如此,由于结构体的副本作为这些函数的接收器来传递,我们将更改只在函数作用域内有效的副本,然后返回它。这是在另一个包中调用它的示例:

2020/06/03 22:22:40 {0}
2020/06/03 22:22:40 {10}

命令行上的结果会是这样的:

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
}

如你所见,尽管通过变量 acc 调用了 Deposit 方法,但实际上变量并没有改变,它返回了新的  Account 副本(分配给 acc2

🎜가장 기본적인 개념은 개체(또는 구조체 본문)를 만들고 다시는 변경하지 않는 것입니다. 그러나 우리는 엔터티 상태가 중요한 애플리케이션에 대해 작업하는 경우가 많습니다. 그러나 프로그램에서 엔터티 상태와 엔터티의 내부 표현은 다릅니다. 불변성을 사용할 때 엔터티에 여러 상태를 할당할 수 있습니다. 즉, 생성된 구조는 변경되지 않지만 복사본은 변경됩니다. 이는 구조의 각 필드를 복사하는 기능을 수동으로 구현해야 한다는 의미는 아닙니다. 🎜🎜대신 함수를 호출할 때 값을 복사하는 Go 언어의 기본 동작을 사용할 수 있습니다. 엔터티의 상태를 변경하는 작업의 경우 구조를 매개변수(또는 함수 수신자)로 수신하고 실행 후 변경된 버전을 반환하는 함수를 만들 수 있습니다. 함수 호출자가 인수로 전달한 변수를 변경하지 않고도 복사본에서 무엇이든 변경할 수 있기 때문에 이는 매우 강력한 기술입니다. 이는 부작용이 없고 예측 가능한 행동을 의미합니다. 동일한 구조가 동시 함수에 전달되면 각 구조는 해당 구조에 대한 포인터가 아닌 복사본을 받습니다. 🎜🎜슬라이싱 기능을 사용하면 [append](https://golang.org/pkg/builtin/#append) 기능에 이 동작이 적용되는 것을 볼 수 있습니다. 🎜🎜Back to us 이 예에서는
Amount 유형의 balance 필드가 포함된 Account 유형을 구현해 보겠습니다. 동시에 DepositWithdraw 메서드를 추가하여 Account 엔터티의 상태를 변경합니다. 🎜
func sum(a, b, rand **int**) **int** {
   return a + b + rand
}
🎜우리가 생성한 메서드를 살펴보면 함수의 수신자인 Account 구조의 상태를 실제로 변경하고 있는 것으로 보입니다. 포인터를 사용하지 않기 때문에 그렇지 않으며 구조체의 복사본이 이러한 함수의 수신자로 전달되므로 함수 범위 내에서만 유효한 복사본을 변경한 다음 반환합니다. 다음은 다른 패키지에서 이를 호출하는 예입니다. 🎜rrreee🎜명령줄의 결과는 다음과 같습니다. 🎜rrreee🎜보시다시피 Deposit은 <code>acc 변수를 통해 호출됩니다. 메서드를 사용했지만 변수가 실제로 변경되지 않은 경우 변경된 필드가 포함된 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 learnku.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제