Go 프로그래밍 언어: Golang 입문서

게시 됨: 2022-03-11

Go 프로그래밍 언어란 무엇입니까?

상대적으로 새로운 Go 프로그래밍 언어는 많은 좋은 기능을 제공하고 의도적으로 많은 나쁜 기능을 생략하면서 환경의 한가운데에 깔끔하게 자리 잡고 있습니다. 빠른 컴파일, 빠른 실행, 런타임 및 가비지 수집 포함, 간단한 정적 유형 시스템 및 동적 인터페이스, 우수한 표준 라이브러리가 있습니다. 이것이 바로 많은 개발자들이 Go 프로그래밍을 배우고 싶어하는 이유입니다.

Golang 튜토리얼: 로고 일러스트레이션

이동 및 OOP

OOP는 Go에서 의도적으로 생략한 기능 중 하나입니다. 하위 분류가 없기 때문에 상속 다이아몬드나 슈퍼 호출, 가상 메서드가 없습니다. 그러나 OOP의 유용한 부분 중 많은 부분을 다른 방식으로 사용할 수 있습니다.

*Mixins*는 익명으로 구조체를 포함하여 사용할 수 있으므로 해당 메서드를 포함하는 구조체에서 직접 호출할 수 있습니다(포함 참조). 이러한 방식으로 메서드를 승격하는 것을 *전달*이라고 하며 서브클래싱과 동일하지 않습니다. 메서드는 여전히 내부에 포함된 구조체에서 호출됩니다.

임베딩은 또한 다형성을 의미하지 않습니다. 'A'는 'B'를 가질 수 있지만 이것이 'B'라는 의미 아닙니다. 'B'를 취하는 함수는 대신 'A'를 취하지 않습니다. 이를 위해 우리는 인터페이스 가 필요합니다. 이에 대해서는 나중에 간단히 다루게 될 것입니다.

한편, Golang은 혼란과 버그를 유발할 수 있는 기능에 대해 확고한 입장을 취하고 있습니다. 구성 및 간단한 인터페이스를 위해 상속 및 다형성과 같은 OOP 관용구를 생략합니다. 반환 값의 명시적 오류를 위해 예외 처리를 경시합니다. gofmt 도구에 의해 시행되는 Go 코드를 배치하는 올바른 방법은 정확히 한 가지입니다. 등등.

Golang을 배우는 이유

Go는 또한 동시 프로그램 작성을 위한 훌륭한 언어입니다. 즉, 독립적으로 실행되는 많은 부분이 있는 프로그램입니다. 분명한 예는 웹 서버입니다. 모든 요청은 개별적으로 실행되지만 요청은 종종 세션, 캐시 또는 알림 대기열과 같은 리소스를 공유해야 합니다. 이는 숙련된 Go 프로그래머가 해당 리소스에 대한 동시 액세스를 처리해야 함을 의미합니다.

Golang에는 동시성을 처리하기 위한 뛰어난 저수준 기능 세트가 있지만 직접 사용하는 것은 복잡할 수 있습니다. 많은 경우에 이러한 저수준 메커니즘에 대한 소수의 재사용 가능한 추상화는 삶을 훨씬 더 쉽게 만듭니다.

오늘의 Go 프로그래밍 튜토리얼에서 우리는 그러한 추상화 중 하나를 살펴볼 것입니다. 모든 데이터 구조를 트랜잭션 서비스 로 바꿀 수 있는 래퍼입니다. 우리는 Fund 유형을 예로 사용합니다. 즉, 잔액을 확인하고 인출할 수 있는 스타트업의 남은 자금을 위한 간단한 저장소입니다.

이것을 실제로 시연하기 위해 우리는 작은 단계로 서비스를 구축하고 도중에 엉망이 된 다음 다시 정리합니다. Go 튜토리얼을 진행하면서 다음과 같은 멋진 Go 언어 기능을 많이 접하게 될 것입니다.

  • 구조체 유형 및 메서드
  • 단위 테스트 및 벤치마크
  • 고루틴과 채널
  • 인터페이스 및 동적 타이핑

단순 펀드 구축

스타트업의 자금을 추적하는 코드를 작성해 보겠습니다. 펀드는 주어진 잔액으로 시작하며 돈은 인출만 가능합니다(수익은 나중에 알아낼 것입니다).

이 그래픽은 Go 프로그래밍 언어를 사용하는 간단한 goroutine 예제를 보여줍니다.

Go는 의도적으로 객체 지향 언어가 아닙니다 . 클래스, 객체 또는 상속이 없습니다. 대신, 새로운 자금 구조체를 생성하는 간단한 함수와 두 개의 공개 메서드를 사용하여 Fund 라는 구조체 유형 을 선언합니다.

펀드고

 package funding type Fund struct { // balance is unexported (private), because it's lowercase balance int } // A regular function returning a pointer to a fund func NewFund(initialBalance int) *Fund { // We can return a pointer to a new struct without worrying about // whether it's on the stack or heap: Go figures that out for us. return &Fund{ balance: initialBalance, } } // Methods start with a *receiver*, in this case a Fund pointer func (f *Fund) Balance() int { return f.balance } func (f *Fund) Withdraw(amount int) { f.balance -= amount }

벤치마크를 사용한 테스트

다음으로 Fund 를 테스트할 방법이 필요합니다. 별도의 프로그램을 작성하는 대신 단위 테스트와 벤치마크 모두에 대한 프레임워크를 제공하는 Go의 테스트 패키지를 사용합니다. 우리 Fund 의 단순한 논리는 단위 테스트를 작성할 가치가 없지만 나중에 펀드에 대한 동시 액세스에 대해 많은 이야기를 할 것이기 때문에 벤치마크를 작성하는 것이 합리적입니다.

벤치마크는 단위 테스트와 비슷하지만 동일한 코드를 여러 번 실행하는 루프를 포함합니다(이 경우에는 fund.Withdraw(1) ). 이를 통해 프레임워크는 각 반복에 걸리는 시간을 측정하여 디스크 탐색, 캐시 누락, 프로세스 일정 및 기타 예측할 수 없는 요인과의 일시적인 차이를 평균화할 수 있습니다.

테스트 프레임워크는 각 벤치마크가 최소 1초(기본값) 동안 실행되기를 원합니다. 이를 보장하기 위해 실행이 최소 1초가 걸릴 때까지 벤치마크를 여러 번 호출하고 매번 증가하는 "반복 횟수" 값( bN 필드)을 전달합니다.

현재로서는 벤치마크에서 약간의 돈을 예치한 다음 한 번에 1달러를 인출합니다.

Fund_test.go

 package funding import "testing" func BenchmarkFund(b *testing.B) { // Add as many dollars as we have iterations this run fund := NewFund(bN) // Burn through them one at a time until they are all gone for i := 0; i < bN; i++ { fund.Withdraw(1) } if fund.Balance() != 0 { b.Error("Balance wasn't zero:", fund.Balance()) } }

이제 실행해 보겠습니다.

 $ go test -bench . funding testing: warning: no tests to run PASS BenchmarkWithdrawals 2000000000 1.69 ns/op ok funding 3.576s

잘 됐다. 우리는 20억(!)번의 반복을 실행했고 잔액에 대한 최종 확인이 정확했습니다. 우리는 "실행할 테스트 없음" 경고를 무시할 수 있습니다. 이 경고는 우리가 작성하지 않은 단위 테스트를 나타냅니다(이 튜토리얼의 나중 Go 프로그래밍 예제에서는 경고가 잘립니다).

Go에서 동시 액세스

이제 동시에 인출하는 여러 사용자를 모델링하기 위해 벤치마크를 동시에 만들어 보겠습니다. 그렇게 하기 위해 우리는 10개의 고루틴을 생성하고 그들 각각이 돈의 10분의 1을 인출하도록 할 것입니다.

Go 언어에서 다중 동시 고루틴을 어떻게 구성할까요?

고루틴은 Go 언어의 동시성을 위한 기본 빌딩 블록입니다. 운영 체제가 아닌 Go 런타임에서 관리하는 경량 스레드인 녹색 스레드입니다. 이는 상당한 오버헤드 없이 수천(또는 수백만)을 실행할 수 있음을 의미합니다. 고루틴은 go 키워드로 생성되며 항상 함수(또는 메서드 호출)로 시작합니다.

 // Returns immediately, without waiting for `DoSomething()` to complete go DoSomething()

종종 우리는 몇 줄의 코드로 짧은 일회성 함수를 생성하고자 합니다. 이 경우 함수 이름 대신 클로저를 사용할 수 있습니다.

 go func() { // ... do stuff ... }() // Must be a function *call*, so remember the ()

모든 고루틴이 생성되면 완료될 때까지 기다릴 방법이 필요합니다. channel 을 사용하여 직접 만들 수 있지만 아직 만나지 않았으므로 건너뛸 것입니다.

지금은 바로 이 목적을 위해 존재하는 Go의 표준 라이브러리에 있는 WaitGroup 유형을 사용할 수 있습니다. 우리는 하나(" wg "라고 함)를 만들고 각 작업자를 생성하기 전에 wg.Add(1) 를 호출하여 얼마나 많은 작업자가 있는지 추적합니다. 그런 다음 작업자는 wg.Done() 을 사용하여 다시 보고합니다. 한편 메인 고루틴에서는 wg.Wait() 라고 말하여 모든 작업자가 완료될 때까지 차단할 수 있습니다.

다음 예제의 작업자 고루틴 내부에서 defer 를 사용하여 wg.Done() 을 호출합니다.

defer 는 함수(또는 메서드) 호출을 받아 다른 모든 작업이 완료된 후 현재 함수가 반환되기 직전에 실행합니다. 이것은 정리에 편리합니다.

 func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()

이렇게 하면 가독성을 위해 UnlockLock 과 쉽게 일치시킬 수 있습니다. 더 중요한 것은, 지연된 함수는 메인 함수(다른 언어에서는 try-finally를 통해 처리할 수 있는 것 )에 패닉이 있는 경우에도 실행된다는 것입니다.

마지막으로 지연된 함수는 호출된 역순 으로 실행됩니다. 즉, 중첩된 정리를 멋지게 수행할 수 있습니다(중첩된 gotolabel 의 C 관용구와 유사하지만 훨씬 깔끔함).

 func() { db.Connect() defer db.Disconnect() // If Begin panics, only db.Disconnect() will execute transaction.Begin() defer transaction.Close() // From here on, transaction.Close() will run first, // and then db.Disconnect() // ... }()

자, 이제 새 버전은 다음과 같습니다.

Fund_test.go

 package funding import ( "sync" "testing" ) const WORKERS = 10 func BenchmarkWithdrawals(b *testing.B) { // Skip N = 1 if bN < WORKERS { return } // Add as many dollars as we have iterations this run fund := NewFund(bN) // Casually assume bN divides cleanly dollarsPerFounder := bN / WORKERS // WaitGroup structs don't need to be initialized // (their "zero value" is ready to use). // So, we just declare one and then use it. var wg sync.WaitGroup for i := 0; i < WORKERS; i++ { // Let the waitgroup know we're adding a goroutine wg.Add(1) // Spawn off a founder worker, as a closure go func() { // Mark this worker done when the function finishes defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { fund.Withdraw(1) } }() // Remember to call the closure! } // Wait for all the workers to finish wg.Wait() if fund.Balance() != 0 { b.Error("Balance wasn't zero:", fund.Balance()) } }

우리는 여기서 무슨 일이 일어날지 예측할 수 있습니다. 작업자는 모두 서로의 위에서 Withdraw 를 실행합니다. 그 내부에서 f.balance -= amount 는 잔액을 읽고 1을 뺀 다음 다시 씁니다. 그러나 때로는 두 명 이상의 작업자가 동일한 잔액을 읽고 동일한 빼기를 수행하므로 결국 잘못된 합계가 나옵니다. 오른쪽?

 $ go test -bench . funding BenchmarkWithdrawals 2000000000 2.01 ns/op ok funding 4.220s

아니, 여전히 지나간다. 여기에 무슨 일이 벌어 졌었 나?

고루틴은 녹색 스레드 라는 것을 기억하십시오. 고루틴은 OS가 아니라 Go 런타임에 의해 관리됩니다. 런타임은 사용 가능한 OS 스레드 수에 관계없이 고루틴을 예약합니다. 이 Go 언어 튜토리얼을 작성할 때 Go는 얼마나 많은 OS 스레드를 사용해야 하는지 추측하려고 하지 않으며, 하나 이상을 원하면 그렇게 말해야 합니다. 마지막으로, 현재 런타임은 고루틴을 선점하지 않습니다. 고루틴은 중단 준비가 되었음을 암시하는 작업(예: 채널과의 상호 작용)을 수행할 때까지 계속 실행됩니다.

이 모든 것은 우리의 벤치마크가 현재 동시적이지만 병렬 적이지 않다는 것을 의미합니다. 한 번에 한 명의 작업자만 실행되며 완료될 때까지 실행됩니다. GOMAXPROCS 환경 변수를 통해 더 많은 스레드를 사용하도록 Go에 지시하여 이를 변경할 수 있습니다.

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 account_test.go:39: Balance wasn't zero: 4238 ok funding 0.007s

그게 낫다. 이제 우리는 우리가 예상했던 것처럼 분명히 일부 인출을 잃고 있습니다.

이 Go 프로그래밍 예제에서 여러 병렬 고루틴의 결과는 좋지 않습니다.

서버로 만드세요

이 시점에서 다양한 옵션이 있습니다. 펀드 주위에 명시적 뮤텍스 또는 읽기-쓰기 잠금을 추가할 수 있습니다. 버전 번호로 비교 및 ​​교환을 사용할 수 있습니다. 우리는 총력을 다해 CRDT 방식을 사용할 수 있습니다(아마도 balance 필드를 각 클라이언트에 대한 트랜잭션 목록으로 대체하고 그 목록에서 잔액을 계산).

하지만 지금은 그 어떤 것도 하지 않을 것입니다. 왜냐하면 그것들은 지저분하거나 무섭거나 둘 다이기 때문입니다. 대신에, 우리는 펀드가 서버 가 되어야 한다고 결정할 것입니다. 서버가 뭔가요? 그것은 당신이 이야기하는 것입니다. Go에서는 채널을 통해 이야기합니다.

채널은 고루틴 간의 기본 통신 메커니즘입니다. 값은 채널로 전송되고( channel <- value 사용), 다른 쪽에서 수신될 수 있습니다( value = <- channel ). 채널은 "고루틴 안전"입니다. 즉, 여러 고루틴이 동시에 송수신할 수 있습니다.

버퍼링

통신 채널 버퍼링은 특정 상황에서 성능 최적화가 될 수 있지만 매우 주의해서 사용해야 합니다(및 벤치마킹!).

그러나 통신과 직접적인 관련이 없는 버퍼링된 채널에 대한 용도가 있습니다.

예를 들어 일반적인 제한 관용구는 버퍼 크기가 '10'인 채널을 만든 다음 즉시 10개의 토큰을 보냅니다. 그런 다음 원하는 수의 작업자 고루틴이 생성되고 각각은 작업을 시작하기 전에 채널에서 토큰을 수신하고 나중에 다시 보냅니다. 그러면 일꾼이 아무리 많아도 동시에 10명만 일하게 될 것입니다.

기본적으로 Go 채널은 버퍼링되지 않습니다. 이것은 다른 고루틴이 즉시 그것을 받을 준비가 될 때까지 채널에 값을 보내는 것이 차단된다는 것을 의미합니다. Go는 또한 채널에 대한 고정 버퍼 크기를 지원합니다( make(chan someType, bufferSize) 사용). 그러나 일반적인 사용의 경우 이것은 일반적으로 나쁜 생각 입니다.

각 요청이 출금하는 우리 펀드의 웹서버를 상상해 보십시오. 상황이 매우 바쁠 때 FundServer 는 따라갈 수 없으며 명령 채널로 보내려는 요청이 차단되고 대기하기 시작합니다. 이 시점에서 우리는 서버에서 최대 요청 수를 적용하고 해당 제한을 초과하는 클라이언트에 합리적인 오류 코드(예: 503 Service Unavailable )를 반환할 수 있습니다. 이것은 서버에 과부하가 걸렸을 때 가능한 최선의 동작입니다.

채널에 버퍼링을 추가하면 이 동작이 덜 결정적입니다. 클라이언트가 훨씬 더 일찍 본 정보를 기반으로 처리되지 않은 명령의 긴 대기열로 쉽게 끝날 수 있습니다(아마도 업스트림에서 시간 초과된 요청의 경우). 수신자가 발신자를 따라갈 수 없을 때 TCP를 통해 백프레셔를 적용하는 것과 같은 다른 많은 상황에서도 동일하게 적용됩니다.

어쨌든 Go 예제의 경우 기본 버퍼링되지 않은 동작을 사용합니다.

채널을 사용하여 FundServer 에 명령을 보냅니다. 모든 벤치마크 작업자는 채널에 명령을 보내지만 서버만 명령을 받습니다.

우리는 Fund 유형을 서버 구현으로 직접 전환할 수 있지만 그것은 지저분할 것입니다. 우리는 동시성 처리와 비즈니스 로직을 혼합할 것입니다. 대신 Fund 유형을 그대로 두고 FundServer 를 별도의 래퍼로 만들 것입니다.

다른 서버와 마찬가지로 래퍼에는 명령을 기다리고 차례로 응답하는 메인 루프가 있습니다. 여기서 해결해야 할 세부 사항이 하나 더 있습니다. 명령 유형입니다.

이 Go 프로그래밍 튜토리얼에서 서버로 사용되는 펀드의 다이어그램.

포인터

명령 채널이 명령에 대한 *포인터*를 사용하도록 할 수 있습니다(`chan *TransactionCommand`). 왜 우리는하지 않았습니까?

고루틴 간에 포인터를 전달하는 것은 위험합니다. 고루틴 중 하나가 포인터를 수정할 수 있기 때문입니다. 다른 고루틴이 다른 CPU 코어에서 실행 중일 수 있기 때문에(더 많은 캐시 무효화를 의미함) 효율성이 떨어지는 경우가 많습니다.

가능하면 일반 값을 전달하는 것을 선호합니다.

아래의 다음 섹션에서는 각각 고유한 구조체 유형이 있는 몇 가지 다른 명령을 보낼 것입니다. 우리는 서버의 Commands 채널이 그들 중 하나를 받아들이기를 원합니다. OOP 언어에서는 다형성을 통해 이를 수행할 수 있습니다. 채널이 수퍼클래스를 사용하도록 하고 그 중 개별 명령 유형은 서브클래스였습니다. Go에서는 대신 인터페이스 를 사용합니다.

인터페이스는 메서드 서명 집합입니다. 이러한 모든 메소드를 구현하는 모든 유형은 해당 인터페이스로 처리될 수 있습니다(그렇게 선언되지 않음). 첫 번째 실행에서는 명령 구조체가 실제로 메서드를 노출하지 않으므로 빈 인터페이스인 interface{} 를 사용할 것입니다. 요구 사항이 없기 때문에 모든 값(정수와 같은 기본 값 포함)은 빈 인터페이스를 충족합니다. 이것은 이상적이지 않습니다. 우리는 명령 구조체만 받아들이기를 원하지만 나중에 다시 설명하겠습니다.

지금은 Go 서버용 스캐폴딩을 시작하겠습니다.

서버.고

 package funding type FundServer struct { Commands chan interface{} fund Fund } func NewFundServer(initialBalance int) *FundServer { server := &FundServer{ // make() creates builtins like channels, maps, and slices Commands: make(chan interface{}), fund: NewFund(initialBalance), } // Spawn off the server's main loop immediately go server.loop() return server } func (s *FundServer) loop() { // The built-in "range" clause can iterate over channels, // amongst other things for command := range s.Commands { // Handle the command } }

이제 명령에 대해 몇 가지 Golang 구조체 유형을 추가해 보겠습니다.

 type WithdrawCommand struct { Amount int } type BalanceCommand struct { Response chan int }

WithdrawCommand 에는 인출할 금액만 포함됩니다. 응답이 없습니다. BalanceCommand 에는 응답이 있으므로 이를 보낼 채널이 포함되어 있습니다. 이렇게 하면 나중에 우리 펀드가 순서 없이 응답하기로 결정하더라도 응답이 항상 올바른 위치로 이동합니다.

이제 서버의 메인 루프를 작성할 수 있습니다.

 func (s *FundServer) loop() { for command := range s.Commands { // command is just an interface{}, but we can check its real type switch command.(type) { case WithdrawCommand: // And then use a "type assertion" to convert it withdrawal := command.(WithdrawCommand) s.fund.Withdraw(withdrawal.Amount) case BalanceCommand: getBalance := command.(BalanceCommand) balance := s.fund.Balance() getBalance.Response <- balance default: panic(fmt.Sprintf("Unrecognized command: %v", command)) } } }

흠. 그것은 일종의 추한 것입니다. 유형 어설션을 사용하여 명령 유형을 켜고 충돌할 수 있습니다. 어쨌든 앞서서 서버를 사용하도록 벤치마크를 업데이트합시다.

 func BenchmarkWithdrawals(b *testing.B) { // ... server := NewFundServer(bN) // ... // Spawn off the workers for i := 0; i < WORKERS; i++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { server.Commands <- WithdrawCommand{ Amount: 1 } } }() } // ... balanceResponseChan := make(chan int) server.Commands <- BalanceCommand{ Response: balanceResponseChan } balance := <- balanceResponseChan if balance != 0 { b.Error("Balance wasn't zero:", balance) } }

특히 잔액을 확인할 때 그것은 일종의 추악했습니다. 괜찮아요. 해 보자:

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 465 ns/op ok funding 2.822s

훨씬 더 나은, 우리는 더 이상 인출을 잃지 않습니다. 그러나 코드를 읽기가 점점 어려워지고 있으며 더 심각한 문제가 있습니다. BalanceCommand 를 발행하고 응답을 읽는 것을 잊어버린 경우, 우리 펀드 서버는 그것을 보내려는 시도를 영원히 차단할 것입니다. 좀 정리를 해보자.

서비스로 만드세요

서버는 대화 상대입니다. 서비스가 뭔가요? 서비스는 API와 대화하는 것입니다. 클라이언트 코드가 명령 채널과 직접 작동하도록 하는 대신 채널을 내보내지 않도록(비공개) 만들고 사용 가능한 명령을 함수로 래핑합니다.

 type FundServer struct { commands chan interface{} // Lowercase name, unexported // ... } func (s *FundServer) Balance() int { responseChan := make(chan int) s.commands <- BalanceCommand{ Response: responseChan } return <- responseChan } func (s *FundServer) Withdraw(amount int) { s.commands <- WithdrawCommand{ Amount: amount } }

이제 벤치마크는 server.Withdraw(1)balance := server.Balance() 라고 말할 수 있으며 실수로 잘못된 명령을 보내거나 응답을 읽는 것을 잊어버릴 가능성이 줄어듭니다.

이 샘플 Go 언어 프로그램에서 펀드를 서비스로 사용하는 방법은 다음과 같습니다.

명령에 대한 추가 상용구가 여전히 많이 있지만 나중에 다시 설명하겠습니다.

업무

결국 돈은 항상 부족합니다. 우리 기금이 마지막 10달러까지 떨어지면 인출을 중단하고 그 돈을 공동 피자에 사용하여 축하하거나 위로하는 데 동의합시다. 벤치마크에는 다음이 반영됩니다.

 // Spawn off the workers for i := 0; i < WORKERS; i++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { // Stop when we're down to pizza money if server.Balance() <= 10 { break } server.Withdraw(1) } }() } // ... balance := server.Balance() if balance != 10 { b.Error("Balance wasn't ten dollars:", balance) }

이번에는 정말 결과를 예측할 수 있습니다.

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 fund_test.go:43: Balance wasn't ten dollars: 6 ok funding 0.009s

우리는 처음 시작했던 곳으로 돌아왔습니다. 여러 작업자가 한 번에 잔액을 읽은 다음 모두 업데이트할 수 있습니다. 이를 처리하기 위해 펀드 자체에 minimumBalance 속성과 같은 로직을 추가하거나 WithdrawIfOverXDollars 라는 다른 명령을 추가할 수 있습니다. 둘 다 끔찍한 생각입니다. 우리의 계약은 펀드의 자산이 아니라 우리 사이에 있습니다. 애플리케이션 로직에 보관해야 합니다.

우리에게 정말로 필요한 것은 데이터베이스 트랜잭션 과 같은 의미에서 트랜잭션입니다. 우리 서비스는 한 번에 하나의 명령만 실행하기 때문에 매우 쉽습니다. 콜백(클로저)이 포함된 Transact 명령을 추가합니다. 서버는 자체 고루틴 내에서 해당 콜백을 실행하고 원시 Fund 를 전달합니다. 그러면 콜백은 Fund 로 원하는 모든 작업을 안전하게 수행할 수 있습니다.

세마포어 및 오류

이 다음 예에서 우리는 두 가지 작은 일을 잘못하고 있습니다.

먼저 '완료' 채널을 세마포어로 사용하여 트랜잭션이 완료되면 호출 코드에 알립니다. 괜찮습니다. 하지만 채널 유형이 'bool'인 이유는 무엇입니까? 우리는 "완료"를 의미하기 위해 'true'만 보낼 것입니다('false'를 보내는 것은 무엇을 의미합니까?). 우리가 정말로 원하는 것은 단일 상태 값(값이 없는 값?)입니다. Go에서는 빈 구조체 유형인 `struct{}`를 사용하여 이 작업을 수행할 수 있습니다. 이것은 또한 메모리를 덜 사용하는 이점이 있습니다. 예제에서 우리는 너무 무섭게 보이지 않도록 `bool`을 사용할 것입니다.

둘째, 트랜잭션 콜백은 아무 것도 반환하지 않습니다. 잠시 후 살펴보겠지만 범위 트릭을 사용하여 콜백에서 값을 호출 코드로 가져올 수 있습니다. 그러나 실제 시스템의 트랜잭션은 때때로 실패할 수 있으므로 Go 규칙은 트랜잭션이 '오류'를 반환하도록 하는 것입니다(그런 다음 호출 코드에서 'nil'인지 확인).

생성할 오류가 없기 때문에 현재로서는 그렇게 하지 않습니다.
 // Typedef the callback for readability type Transactor func(fund *Fund) // Add a new command type with a callback and a semaphore channel type TransactionCommand struct { Transactor Transactor Done chan bool } // ... // Wrap it up neatly in an API method, like the other commands func (s *FundServer) Transact(transactor Transactor) { command := TransactionCommand{ Transactor: transactor, Done: make(chan bool), } s.commands <- command <- command.Done } // ... func (s *FundServer) loop() { for command := range s.commands { switch command.(type) { // ... case TransactionCommand: transaction := command.(TransactionCommand) transaction.Transactor(s.fund) transaction.Done <- true // ... } } }

트랜잭션 콜백은 직접적으로 아무 것도 반환하지 않지만 Go 언어를 사용하면 클로저에서 직접 값을 쉽게 얻을 수 있으므로 벤치마크에서 이를 수행하여 돈이 부족할 때 pizzaTime 플래그를 설정합니다.

 pizzaTime := false for i := 0; i < dollarsPerFounder; i++ { server.Transact(func(fund *Fund) { if fund.Balance() <= 10 { // Set it in the outside scope pizzaTime = true return } fund.Withdraw(1) }) if pizzaTime { break } }

작동하는지 확인하십시오.

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s

아무것도 아닌 거래

이제 좀 더 정리할 기회를 찾았을 것입니다. 일반 Transact 명령이 있으므로 WithdrawCommand 또는 BalanceCommand 가 더 이상 필요하지 않습니다. 트랜잭션 측면에서 다시 작성합니다.

 func (s *FundServer) Balance() int { var balance int s.Transact(func(f *Fund) { balance = f.Balance() }) return balance } func (s *FundServer) Withdraw(amount int) { s.Transact(func (f *Fund) { f.Withdraw(amount) }) }

이제 서버가 취하는 유일한 명령은 TransactionCommand 이므로 구현에서 전체 interface{} 혼란을 제거하고 트랜잭션 명령만 수락하도록 할 수 있습니다.

 type FundServer struct { commands chan TransactionCommand fund *Fund } func (s *FundServer) loop() { for transaction := range s.commands { // Now we don't need any type-switch mess transaction.Transactor(s.fund) transaction.Done <- true } }

훨씬 낫다.

여기서 우리가 취할 수 있는 마지막 단계가 있습니다. BalanceWithdraw 에 대한 편의 기능을 제외하고 서비스 구현은 더 이상 Fund 에 연결되지 않습니다. Fund 를 관리하는 대신 interface{} 무엇이든 래핑하는 데 사용할 수 있습니다. 그러나 각 트랜잭션 콜백은 interface{} 실제 값으로 다시 변환해야 합니다.

 type Transactor func(interface{}) server.Transact(func(managedValue interface{}) { fund := managedValue.(*Fund) // Do stuff with fund ... })

이것은 추하고 오류가 발생하기 쉽습니다. 우리가 정말로 원하는 것은 컴파일 타임 제네릭이므로 특정 유형(예: *Fund )에 대한 서버를 "템플릿"할 수 있습니다.

불행히도 Go는 아직 제네릭을 지원하지 않습니다. 누군가가 합리적인 구문과 의미를 파악하면 결국 도착할 것으로 예상됩니다. 그 동안 신중한 인터페이스 디자인은 제네릭의 필요성을 제거하는 경우가 많으며, 제네릭이 없는 경우 유형 주장(런타임에 확인됨)으로 해결할 수 있습니다.

끝났어?

네.

글쎄, 알았어, 아니.

예를 들어:

  • 트랜잭션의 패닉은 전체 서비스를 종료합니다.

  • 시간 초과가 없습니다. 반환되지 않는 트랜잭션은 서비스를 영원히 차단합니다.

  • 펀드가 일부 새로운 필드를 확장하고 트랜잭션을 업데이트하는 동안 트랜잭션이 중단되면 일관성 없는 상태가 됩니다.

  • 트랜잭션은 관리되는 Fund 개체를 누출할 수 있으며 이는 좋지 않습니다.

  • 여러 자금에 걸쳐 거래를 수행하는 합리적인 방법은 없습니다(예: 하나에서 인출하고 다른 자금에 예치). 교착 상태를 허용하기 때문에 트랜잭션을 중첩할 수 없습니다.

  • 트랜잭션을 비동기적으로 실행하려면 이제 새로운 고루틴과 많은 혼란이 필요합니다. 이와 관련하여 장기 거래가 진행되는 동안 다른 곳에서 가장 최근의 Fund 상태를 읽을 수 있기를 원할 것입니다.

다음 Go 프로그래밍 언어 자습서에서는 이러한 문제를 해결하는 몇 가지 방법을 살펴보겠습니다.

관련 항목: 잘 구조화된 논리: Golang OOP 자습서