Linguagem de programação Go: um tutorial introdutório de Golang
Publicados: 2022-03-11O que é a linguagem de programação Go?
A relativamente nova linguagem de programação Go fica bem no meio do cenário, fornecendo muitos recursos bons e omitindo deliberadamente muitos ruins. Ele compila rápido, roda rápido, inclui um tempo de execução e coleta de lixo, tem um sistema de tipo estático simples e interfaces dinâmicas e uma excelente biblioteca padrão. É por isso que tantos desenvolvedores desejam aprender a programar Go.
OOP é um desses recursos que o Go omite deliberadamente. Ele não tem subclasses e, portanto, não há diamantes de herança ou super chamadas ou métodos virtuais para enganar você. Ainda assim, muitas das partes úteis da OOP estão disponíveis de outras maneiras.
*Mixins* estão disponíveis incorporando structs anonimamente, permitindo que seus métodos sejam chamados diretamente no struct que o contém (veja incorporação). Promover métodos dessa maneira é chamado de *encaminhamento*, e não é o mesmo que subclassificar: o método ainda será invocado na estrutura interna e incorporada.
A incorporação também não implica polimorfismo. Embora `A` possa ter um `B`, isso não significa que seja um `B` -- funções que recebem um `B` não terão um `A` em vez disso. Para isso, precisamos de interfaces , que encontraremos brevemente mais tarde.
Enquanto isso, Golang assume uma posição forte em recursos que podem levar a confusão e bugs. Ele omite expressões OOP como herança e polimorfismo, em favor da composição e interfaces simples. Ele minimiza o tratamento de exceções em favor de erros explícitos nos valores de retorno. Existe exatamente uma maneira correta de definir o código Go, aplicado pela ferramenta gofmt
. E assim por diante.
Por que aprender Golang?
Go também é uma ótima linguagem para escrever programas simultâneos : programas com muitas partes em execução independentemente. Um exemplo óbvio é um servidor web: cada solicitação é executada separadamente, mas as solicitações geralmente precisam compartilhar recursos como sessões, caches ou filas de notificação. Isso significa que os programadores Go qualificados precisam lidar com o acesso simultâneo a esses recursos.
Embora Golang tenha um excelente conjunto de recursos de baixo nível para lidar com simultaneidade, usá-los diretamente pode se tornar complicado. Em muitos casos, um punhado de abstrações reutilizáveis sobre esses mecanismos de baixo nível torna a vida muito mais fácil.
No tutorial de programação Go de hoje, veremos uma dessas abstrações: Um wrapper que pode transformar qualquer estrutura de dados em um serviço transacional . Usaremos como exemplo um tipo de Fund
– uma simples loja para o financiamento restante da nossa startup, onde podemos consultar o saldo e fazer saques.
Para demonstrar isso na prática, vamos construir o serviço em pequenos passos, fazendo uma bagunça ao longo do caminho e depois limpando-o novamente. À medida que avançamos em nosso tutorial Go, encontraremos muitos recursos interessantes da linguagem Go, incluindo:
- Tipos e métodos de estrutura
- Testes unitários e benchmarks
- Goroutinas e canais
- Interfaces e tipagem dinâmica
Construindo um fundo simples
Vamos escrever algum código para rastrear o financiamento da nossa startup. O fundo começa com um determinado saldo, e o dinheiro só pode ser retirado (vamos descobrir a receita mais tarde).
Go não é deliberadamente uma linguagem orientada a objetos: não há classes, objetos ou herança. Em vez disso, declararemos um tipo de estrutura chamado Fund
, com uma função simples para criar novas estruturas de fundos e dois métodos públicos.
fund.go
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 }
Teste com benchmarks
Em seguida, precisamos de uma maneira de testar Fund
. Em vez de escrever um programa separado, usaremos o pacote de testes do Go, que fornece uma estrutura para testes de unidade e benchmarks. A lógica simples em nosso Fund
não vale a pena escrever testes de unidade, mas como falaremos muito sobre acesso simultâneo ao fundo mais tarde, escrever um benchmark faz sentido.
Benchmarks são como testes de unidade, mas incluem um loop que executa o mesmo código muitas vezes (no nosso caso, fund.Withdraw(1)
). Isso permite que a estrutura calcule quanto tempo cada iteração leva, calculando a média das diferenças transitórias de buscas de disco, faltas de cache, agendamento de processos e outros fatores imprevisíveis.
A estrutura de teste deseja que cada benchmark seja executado por pelo menos 1 segundo (por padrão). Para garantir isso, ele chamará o benchmark várias vezes, passando um valor crescente de “número de iterações” a cada vez (o campo bN
), até que a execução leve pelo menos um segundo.
Por enquanto, nosso benchmark apenas depositará algum dinheiro e depois o retirará um dólar de cada vez.
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()) } }
Agora vamos executá-lo:
$ go test -bench . funding testing: warning: no tests to run PASS BenchmarkWithdrawals 2000000000 1.69 ns/op ok funding 3.576s
Correu bem. Executamos dois bilhões (!) de iterações, e a verificação final do saldo estava correta. Podemos ignorar o aviso “sem testes para executar”, que se refere aos testes de unidade que não escrevemos (em exemplos de programação Go posteriores neste tutorial, o aviso é cortado).
Acesso simultâneo em Go
Agora vamos fazer o benchmark simultâneo, para modelar diferentes usuários fazendo saques ao mesmo tempo. Para fazer isso, vamos gerar dez goroutines e fazer com que cada uma delas retire um décimo do dinheiro.
Goroutines são o bloco de construção básico para simultaneidade na linguagem Go. Eles são threads verdes – threads leves gerenciados pelo runtime Go, não pelo sistema operacional. Isso significa que você pode executar milhares (ou milhões) deles sem nenhuma sobrecarga significativa. Goroutines são geradas com a palavra-chave go
e sempre começam com uma função (ou chamada de método):
// Returns immediately, without waiting for `DoSomething()` to complete go DoSomething()
Muitas vezes, queremos gerar uma função única e curta com apenas algumas linhas de código. Nesse caso, podemos usar uma closure em vez de um nome de função:
go func() { // ... do stuff ... }() // Must be a function *call*, so remember the ()
Uma vez que todas as nossas goroutines são geradas, precisamos esperar que elas terminem. Poderíamos construir um usando canais , mas ainda não os encontramos, então isso seria pular adiante.
Por enquanto, podemos apenas usar o tipo WaitGroup
na biblioteca padrão do Go, que existe para esse propósito. Vamos criar um (chamado “ wg
”) e chamar wg.Add(1)
antes de gerar cada trabalhador, para manter o controle de quantos existem. Em seguida, os trabalhadores reportarão usando wg.Done()
. Enquanto isso, na goroutine principal, podemos apenas dizer wg.Wait()
para bloquear até que todos os trabalhadores tenham terminado.
Dentro das goroutines do trabalhador em nosso próximo exemplo, usaremos defer para chamar defer
wg.Done()
.
defer
recebe uma chamada de função (ou método) e a executa imediatamente antes do retorno da função atual, depois que todo o resto é feito. Isso é útil para limpeza:
func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()
Dessa forma, podemos combinar facilmente o Unlock
com seu Lock
, para facilitar a leitura. Mais importante, uma função adiada será executada mesmo se houver um pânico na função principal (algo que podemos manipular via try-finally em outras linguagens).
Por fim, as funções adiadas serão executadas na ordem inversa à qual foram chamadas, o que significa que podemos fazer uma limpeza aninhada bem (semelhante ao idioma C de goto
s e label
aninhados, mas muito mais organizado):
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() // ... }()
OK, então com tudo isso dito, aqui está a nova versão:
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()) } }
Podemos prever o que vai acontecer aqui. Todos os trabalhadores executarão Withdraw
uns sobre os outros. Dentro dele, f.balance -= amount
lerá o saldo, subtrairá um e depois o escreverá de volta. Mas às vezes dois ou mais trabalhadores lêem o mesmo saldo e fazem a mesma subtração, e acabamos com o total errado. Certo?
$ go test -bench . funding BenchmarkWithdrawals 2000000000 2.01 ns/op ok funding 4.220s
Não, ainda passa. O que aconteceu aqui?
Lembre-se de que as goroutines são threads verdes – elas são gerenciadas pelo runtime Go, não pelo SO. O runtime agenda goroutines em quantos threads de SO ele tiver disponível. No momento de escrever este tutorial da linguagem Go, Go não tenta adivinhar quantos threads de SO ele deve usar e, se quisermos mais de um, temos que dizer isso. Finalmente, o tempo de execução atual não antecipa goroutines – uma goroutine continuará sendo executada até que faça algo que sugira que está pronta para uma pausa (como interagir com um canal).
Tudo isso significa que, embora nosso benchmark agora seja concorrente, ele não é paralelo . Apenas um de nossos trabalhadores funcionará de cada vez, e funcionará até que seja feito. Podemos mudar isso dizendo ao Go para usar mais threads, por meio da variável de ambiente GOMAXPROCS
.
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 account_test.go:39: Balance wasn't zero: 4238 ok funding 0.007s
Isso é melhor. Agora, obviamente, estamos perdendo algumas de nossas retiradas, como esperávamos.
Torná-lo um servidor
Neste ponto temos várias opções. Poderíamos adicionar um mutex explícito ou um bloqueio de leitura e gravação ao redor do fundo. Poderíamos usar um compare-and-swap com um número de versão. Poderíamos usar um esquema CRDT (talvez substituindo o campo balance
por listas de transações para cada cliente e calculando o saldo a partir delas).
Mas não faremos nenhuma dessas coisas agora, porque são confusas ou assustadoras ou as duas coisas. Em vez disso, decidiremos que um fundo deve ser um servidor . O que é um servidor? É algo que você fala. Em Go, as coisas falam por meio de canais.
Os canais são o mecanismo básico de comunicação entre goroutines. Os valores são enviados para o canal (com channel <- value
), e podem ser recebidos do outro lado (com value = <- channel
). Os canais são “goroutine safe”, o que significa que qualquer número de goroutines pode enviar e receber deles ao mesmo tempo.
Os canais de comunicação de buffer podem ser uma otimização de desempenho em determinadas circunstâncias, mas devem ser usados com muito cuidado (e benchmarking!).
No entanto, existem usos para canais em buffer que não são diretamente sobre comunicação.
Por exemplo, um idioma de limitação comum cria um canal com (por exemplo) tamanho de buffer `10` e então envia dez tokens para ele imediatamente. Qualquer número de goroutines de trabalhador é gerado, e cada um recebe um token do canal antes de iniciar o trabalho e o envia de volta depois. Então, por mais trabalhadores que existam, apenas dez estarão trabalhando ao mesmo tempo.
Por padrão, os canais Go são sem buffer . Isso significa que o envio de um valor para um canal será bloqueado até que outra goroutine esteja pronta para recebê-lo imediatamente. Go também suporta tamanhos de buffer fixos para canais (usando make(chan someType, bufferSize)
). No entanto, para uso normal, isso geralmente é uma má ideia .
Imagine um servidor web para nosso fundo, onde cada solicitação faz um saque. Quando as coisas estiverem muito ocupadas, o FundServer
não poderá acompanhar, e as solicitações que tentam enviar para seu canal de comando começarão a bloquear e aguardar. Nesse ponto, podemos impor uma contagem máxima de solicitações no servidor e retornar um código de erro sensato (como 503 Service Unavailable
) para clientes acima desse limite. Este é o melhor comportamento possível quando o servidor está sobrecarregado.

Adicionar buffer aos nossos canais tornaria esse comportamento menos determinístico. Poderíamos facilmente acabar com longas filas de comandos não processados com base em informações que o cliente viu muito antes (e talvez para solicitações que já haviam expirado no upstream). O mesmo se aplica em muitas outras situações, como aplicar contrapressão sobre o TCP quando o receptor não consegue acompanhar o remetente.
De qualquer forma, para nosso exemplo Go, manteremos o comportamento sem buffer padrão.
Usaremos um canal para enviar comandos ao nosso FundServer
. Cada trabalhador de benchmark enviará comandos para o canal, mas apenas o servidor os receberá.
Poderíamos transformar nosso tipo de fundo em uma implementação de servidor diretamente, mas isso seria confuso – estaríamos misturando manipulação de simultaneidade e lógica de negócios. Em vez disso, deixaremos o tipo de fundo exatamente como está e faremos FundServer
um wrapper separado em torno dele.
Como qualquer servidor, o wrapper terá um loop principal no qual espera por comandos e responde a cada um deles. Há mais um detalhe que precisamos abordar aqui: O tipo dos comandos.
Poderíamos ter feito nosso canal de comandos pegar *ponteiros* para comandos (`chan *TransactionCommand`). Por que não fizemos?
Passar ponteiros entre goroutines é arriscado, porque qualquer goroutine pode modificá-lo. Também costuma ser menos eficiente, porque a outra goroutine pode estar sendo executada em um núcleo de CPU diferente (o que significa mais invalidação de cache).
Sempre que possível, prefira passar valores simples.
Na próxima seção abaixo, enviaremos vários comandos diferentes, cada um com seu próprio tipo de estrutura. Queremos que o canal de Comandos do servidor aceite qualquer um deles. Em uma linguagem OOP, podemos fazer isso por meio de polimorfismo: fazer com que o canal receba uma superclasse, da qual os tipos de comando individuais sejam subclasses. Em Go, usamos interfaces em vez disso.
Uma interface é um conjunto de assinaturas de métodos. Qualquer tipo que implemente todos esses métodos pode ser tratado como essa interface (sem ser declarado para isso). Para nossa primeira execução, nossas estruturas de comando não expõem nenhum método, então usaremos a interface vazia, interface{}
. Como não possui requisitos, qualquer valor (incluindo valores primitivos como inteiros) satisfaz a interface vazia. Isso não é o ideal – queremos apenas aceitar estruturas de comando – mas voltaremos a isso mais tarde.
Por enquanto, vamos começar com o scaffolding do nosso servidor Go:
servidor.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 } }
Agora vamos adicionar alguns tipos de estrutura Golang para os comandos:
type WithdrawCommand struct { Amount int } type BalanceCommand struct { Response chan int }
O WithdrawCommand
contém apenas o valor a ser retirado. Não há resposta. O BalanceCommand
tem uma resposta, então inclui um canal para enviá-la. Isso garante que as respostas sempre cheguem ao lugar certo, mesmo que nosso fundo decida posteriormente responder fora de ordem.
Agora podemos escrever o loop principal do servidor:
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)) } } }
Hmm. Isso é meio feio. Estamos ligando o tipo de comando, usando asserções de tipo e possivelmente travando. Vamos seguir em frente de qualquer maneira e atualizar o benchmark para usar o servidor.
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) } }
Isso foi meio feio também, especialmente quando verificamos o saldo. Deixa pra lá. Vamos tentar:
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 465 ns/op ok funding 2.822s
Muito melhor, não estamos mais perdendo saques. Mas o código está ficando difícil de ler e há problemas mais sérios. Se emitirmos um BalanceCommand
e esquecermos de ler a resposta, nosso servidor de fundos bloqueará para sempre a tentativa de enviá-lo. Vamos limpar um pouco as coisas.
Faça disso um serviço
Um servidor é algo com o qual você fala. O que é um serviço? Um serviço é algo com o qual você conversa com uma API . Em vez de fazer com que o código do cliente funcione diretamente com o canal de comando, tornaremos o canal não exportado (privado) e agruparemos os comandos disponíveis em funções.
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 } }
Agora nosso benchmark pode dizer server.Withdraw(1)
e balance := server.Balance()
, e há menos chance de acidentalmente enviar comandos inválidos ou esquecer de ler as respostas.
Ainda há muito clichê extra para os comandos, mas voltaremos a isso mais tarde.
Transações
Eventualmente, o dinheiro sempre acaba. Vamos concordar que vamos parar de sacar quando nosso fundo estiver nos últimos dez dólares e gastar esse dinheiro em uma pizza comunitária para comemorar ou lamentar. Nosso benchmark refletirá isso:
// 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) }
Desta vez podemos realmente prever o resultado.
$ 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
Estamos de volta ao ponto de partida – vários funcionários podem ler o saldo de uma só vez e, em seguida, todos o atualizam. Para lidar com isso, poderíamos adicionar alguma lógica no próprio fundo, como uma propriedade minimumBalance
, ou adicionar outro comando chamado WithdrawIfOverXDollars
. Essas duas ideias são terríveis. Nosso acordo é entre nós, não é propriedade do fundo. Devemos mantê-lo na lógica do aplicativo.
O que realmente precisamos é de transações , no mesmo sentido das transações de banco de dados. Como nosso serviço executa apenas um comando por vez, isso é super fácil. Adicionaremos um comando Transact
que contém um retorno de chamada (um encerramento). O servidor executará esse retorno de chamada dentro de sua própria goroutine, passando o Fund
bruto. O retorno de chamada pode fazer o que quiser com o Fund
com segurança.
Neste próximo exemplo, estamos fazendo duas pequenas coisas erradas.
Primeiro, estamos usando um canal `Done` como um semáforo para informar o código de chamada quando sua transação for concluída. Tudo bem, mas por que o tipo de canal é `bool`? Nós só enviaremos `true` para significar "feito" (o que enviar `false` significaria?). O que realmente queremos é um valor de estado único (um valor que não tem valor?). Em Go, podemos fazer isso usando o tipo struct vazio: `struct{}`. Isso também tem a vantagem de usar menos memória. No exemplo vamos ficar com `bool` para não parecer muito assustador.
Segundo, nosso callback de transação não está retornando nada. Como veremos em breve, podemos obter valores do retorno de chamada para o código de chamada usando truques de escopo. No entanto, as transações em um sistema real presumivelmente falhariam algumas vezes, então a convenção Go seria fazer com que a transação retornasse um 'erro' (e então verificasse se era 'nil' no código de chamada).
Também não estamos fazendo isso por enquanto, pois não temos erros para gerar.
// 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 // ... } } }
Nossos callbacks de transação não retornam nada diretamente, mas a linguagem Go facilita a obtenção de valores de um encerramento diretamente, então faremos isso no benchmark para definir o sinalizador pizzaTime
quando o dinheiro acabar:
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 } }
E verifique se funciona:
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s
Nada além de transações
Você pode ter visto uma oportunidade de limpar as coisas um pouco mais agora. Como temos um comando Transact
genérico, não precisamos mais de WithdrawCommand
ou BalanceCommand
. Vamos reescrevê-los em termos de transações:
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) }) }
Agora, o único comando que o servidor usa é TransactionCommand
, então podemos remover toda a confusão interface{}
em sua implementação e fazer com que ele aceite apenas comandos de transação:
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 } }
Muito melhor.
Há um passo final que poderíamos dar aqui. Além de suas funções de conveniência para Balance
e Withdraw
, a implementação do serviço não está mais vinculada ao Fund
. Em vez de gerenciar um Fund
, ele pode gerenciar uma interface{}
e ser usado para envolver qualquer coisa . No entanto, cada retorno de chamada de transação teria que converter a interface{}
de volta em um valor real:
type Transactor func(interface{}) server.Transact(func(managedValue interface{}) { fund := managedValue.(*Fund) // Do stuff with fund ... })
Isso é feio e propenso a erros. O que realmente queremos são genéricos em tempo de compilação, para que possamos “modelar” um servidor para um tipo específico (como *Fund
).
Infelizmente, Go não suporta genéricos – ainda. Espera-se que chegue eventualmente, assim que alguém descobrir alguma sintaxe e semântica sensata para ele. Enquanto isso, o design cuidadoso da interface geralmente elimina a necessidade de genéricos e, quando isso não acontece, podemos nos virar com asserções de tipo (que são verificadas em tempo de execução).
Terminamos?
sim.
Bem, ok, não.
Por exemplo:
Um pânico em uma transação matará todo o serviço.
Não há tempos limite. Uma transação que nunca retorna bloqueará o serviço para sempre.
Se nosso Fundo aumentar alguns novos campos e uma transação falhar no meio da atualização, teremos um estado inconsistente.
As transações podem vazar o objeto
Fund
gerenciado, o que não é bom.Não há uma maneira razoável de fazer transações em vários fundos (como sacar de um e depositar em outro). Não podemos simplesmente aninhar nossas transações porque isso permitiria deadlocks.
Executar uma transação de forma assíncrona agora requer uma nova goroutine e muita confusão. Da mesma forma, provavelmente queremos ser capazes de ler o estado mais recente do
Fund
de outro lugar enquanto uma transação de longa duração está em andamento.
Em nosso próximo tutorial da linguagem de programação Go, veremos algumas maneiras de resolver esses problemas.