Lenguaje de programación Go: un tutorial introductorio de Golang

Publicado: 2022-03-11

¿Qué es el lenguaje de programación Go?

El relativamente nuevo lenguaje de programación Go se encuentra perfectamente en el medio del paisaje, brindando muchas características buenas y omitiendo deliberadamente muchas de las malas. Compila rápido, se ejecuta rápidamente, incluye tiempo de ejecución y recolección de elementos no utilizados, tiene un sistema de tipo estático simple e interfaces dinámicas, y una biblioteca estándar excelente. Esta es la razón por la que tantos desarrolladores están ansiosos por aprender a programar Go.

Tutorial de Golang: ilustración del logotipo

Ir y OOP

OOP es una de esas características que Go omite deliberadamente. No tiene subclases, por lo que no hay diamantes heredados ni superllamadas ni métodos virtuales para hacerte tropezar. Aun así, muchas de las partes útiles de OOP están disponibles de otras formas.

*Mixins* están disponibles incorporando estructuras de forma anónima, lo que permite llamar a sus métodos directamente en la estructura contenedora (ver incrustación). Promover métodos de esta manera se llama *reenvío*, y no es lo mismo que crear subclases: el método aún se invocará en la estructura interna incrustada.

La incrustación tampoco implica polimorfismo. Mientras que 'A' puede tener una 'B', eso no significa que sea una 'B' -- las funciones que toman una 'B' no tomarán una 'A' en su lugar. Para eso, necesitamos interfaces , que veremos brevemente más adelante.

Mientras tanto, Golang toma una posición fuerte en las características que pueden generar confusión y errores. Omite modismos de programación orientada a objetos como la herencia y el polimorfismo, a favor de la composición y las interfaces simples. Minimiza el manejo de excepciones a favor de errores explícitos en los valores devueltos. Hay exactamente una forma correcta de diseñar el código Go, aplicada por la herramienta gofmt . Y así.

¿Por qué aprender Golang?

Go también es un gran lenguaje para escribir programas concurrentes : programas con muchas partes que se ejecutan de forma independiente. Un ejemplo obvio es un servidor web: cada solicitud se ejecuta por separado, pero las solicitudes a menudo necesitan compartir recursos como sesiones, cachés o colas de notificación. Esto significa que los programadores expertos en Go deben lidiar con el acceso simultáneo a esos recursos.

Si bien Golang tiene un excelente conjunto de funciones de bajo nivel para manejar la concurrencia, usarlas directamente puede volverse complicado. En muchos casos, un puñado de abstracciones reutilizables sobre esos mecanismos de bajo nivel hace la vida mucho más fácil.

En el tutorial de programación de Go de hoy, veremos una de esas abstracciones: un contenedor que puede convertir cualquier estructura de datos en un servicio transaccional . Usaremos un tipo de Fund como ejemplo: una tienda simple para los fondos restantes de nuestra startup, donde podemos verificar el saldo y hacer retiros.

Para demostrar esto en la práctica, construiremos el servicio en pequeños pasos, creando un desorden en el camino y luego limpiándolo nuevamente. A medida que avanzamos en nuestro tutorial de Go, encontraremos muchas características interesantes del lenguaje Go, que incluyen:

  • Tipos de estructuras y métodos.
  • Pruebas unitarias y puntos de referencia
  • Rutinas y canales
  • Interfaces y escritura dinámica

Construyendo un fondo simple

Escribamos un código para rastrear la financiación de nuestra startup. El fondo comienza con un saldo determinado y el dinero solo se puede retirar (veremos los ingresos más adelante).

Este gráfico muestra un ejemplo simple de goroutine usando el lenguaje de programación Go.

Go no es deliberadamente un lenguaje orientado a objetos: no hay clases, objetos ni herencia. En su lugar, declararemos un tipo de estructura llamado Fund , con una función simple para crear nuevas estructuras de fondos y dos métodos públicos.

fondo.ir

 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 }

Pruebas con puntos de referencia

A continuación, necesitamos una forma de probar Fund . En lugar de escribir un programa separado, usaremos el paquete de prueba de Go, que proporciona un marco tanto para las pruebas unitarias como para los puntos de referencia. No vale la pena escribir pruebas unitarias para la lógica simple de nuestro Fund , pero dado que hablaremos mucho sobre el acceso simultáneo al fondo más adelante, tiene sentido escribir un índice de referencia.

Los puntos de referencia son como pruebas unitarias, pero incluyen un ciclo que ejecuta el mismo código muchas veces (en nuestro caso, fund.Withdraw(1) ). Esto permite que el marco cronometre el tiempo que toma cada iteración, promediando las diferencias transitorias de las búsquedas en el disco, las fallas de caché, la programación de procesos y otros factores impredecibles.

El marco de prueba quiere que cada punto de referencia se ejecute durante al menos 1 segundo (de forma predeterminada). Para garantizar esto, llamará al punto de referencia varias veces, pasando un valor de "número de iteraciones" cada vez mayor (el campo bN ), hasta que la ejecución tarde al menos un segundo.

Por ahora, nuestro punto de referencia solo depositará algo de dinero y luego lo retirará un dólar a la 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()) } }

Ahora vamos a ejecutarlo:

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

Eso salió bien. Ejecutamos dos mil millones (!) de iteraciones, y la verificación final del saldo fue correcta. Podemos ignorar la advertencia "no hay pruebas para ejecutar", que se refiere a las pruebas unitarias que no escribimos (en ejemplos posteriores de programación de Go en este tutorial, la advertencia se elimina).

Acceso simultáneo en Go

Ahora hagamos que el punto de referencia sea simultáneo, para modelar diferentes usuarios que realizan retiros al mismo tiempo. Para hacer eso, generaremos diez goroutines y haremos que cada uno de ellos retire una décima parte del dinero.

¿Cómo estructuraríamos múltiples rutinas concurrentes en el lenguaje Go?

Goroutines son el bloque de construcción básico para la concurrencia en el lenguaje Go. Son subprocesos verdes: subprocesos livianos administrados por el tiempo de ejecución de Go, no por el sistema operativo. Esto significa que puede ejecutar miles (o millones) de ellos sin gastos generales significativos. Las rutinas Gor se generan con la palabra clave go y siempre comienzan con una función (o llamada de método):

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

A menudo, queremos generar una función corta de una sola vez con solo unas pocas líneas de código. En este caso podemos usar un cierre en lugar de un nombre de función:

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

Una vez que se generan todas nuestras gorutinas, necesitamos una forma de esperar a que terminen. Podríamos construir uno nosotros mismos usando canales , pero aún no los hemos encontrado, por lo que sería saltarnos adelante.

Por ahora, solo podemos usar el tipo WaitGroup en la biblioteca estándar de Go, que existe para este mismo propósito. Crearemos uno (llamado " wg ") y llamaremos a wg.Add(1) antes de generar cada trabajador, para realizar un seguimiento de cuántos hay. Luego, los trabajadores informarán usando wg.Done() . Mientras tanto, en la rutina principal, podemos simplemente decir wg.Wait() para bloquear hasta que todos los trabajadores hayan terminado.

Dentro de las rutinas de trabajo en nuestro siguiente ejemplo, usaremos defer para llamar a wg.Done() .

defer toma una llamada de función (o método) y la ejecuta inmediatamente antes de que regrese la función actual, después de que se haya hecho todo lo demás. Esto es útil para la limpieza:

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

De esta manera, podemos hacer coincidir fácilmente el Unlock con su Lock , para facilitar la lectura. Más importante aún, una función diferida se ejecutará incluso si hay pánico en la función principal (algo que podríamos manejar a través de try-finally en otros idiomas).

Por último, las funciones diferidas se ejecutarán en el orden inverso al que fueron llamadas, lo que significa que podemos hacer una limpieza anidada muy bien (similar al modismo C de goto s y label s anidados, pero mucho más limpio):

 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() // ... }()

Bien, con todo lo dicho, aquí está la nueva versión:

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 predecir lo que sucederá aquí. Todos los trabajadores ejecutarán Withdraw uno encima del otro. Dentro de él, f.balance -= amount leerá el saldo, restará uno y luego lo escribirá de nuevo. Pero a veces, dos o más trabajadores leerán el mismo saldo y harán la misma resta, y terminaremos con un total incorrecto. ¿Derecha?

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

No, todavía pasa. ¿Que pasó aquí?

Recuerde que las gorutinas son subprocesos verdes : son administradas por el tiempo de ejecución de Go, no por el sistema operativo. El tiempo de ejecución programa las rutinas en todos los subprocesos del sistema operativo que tenga disponibles. Al momento de escribir este tutorial del lenguaje Go, Go no intenta adivinar cuántos subprocesos del sistema operativo debe usar, y si queremos más de uno, tenemos que decirlo. Por último, el tiempo de ejecución actual no se adelanta a las gorutinas: una gorutina continuará ejecutándose hasta que haga algo que sugiera que está lista para un descanso (como interactuar con un canal).

Todo esto significa que, aunque nuestro punto de referencia ahora es concurrente, no es paralelo . Solo uno de nuestros trabajadores se ejecutará a la vez, y se ejecutará hasta que termine. Podemos cambiar esto diciéndole a Go que use más subprocesos, a través de la variable de entorno 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

Eso es mejor. Ahora obviamente estamos perdiendo algunos de nuestros retiros, como esperábamos.

En este ejemplo de programación de Go, el resultado de múltiples gorutinas paralelas no es favorable.

Conviértelo en un servidor

En este punto tenemos varias opciones. Podríamos agregar un mutex explícito o un bloqueo de lectura y escritura alrededor del fondo. Podríamos usar una comparación e intercambio con un número de versión. Podríamos hacer todo lo posible y usar un esquema CRDT (quizás reemplazando el campo de balance con listas de transacciones para cada cliente y calculando el saldo a partir de ellas).

Pero no haremos ninguna de esas cosas ahora, porque son desordenadas o dan miedo o ambas cosas. En su lugar, decidiremos que un fondo debe ser un servidor . ¿Qué es un servidor? Es algo con lo que hablas. En Go, las cosas hablan a través de canales.

Los canales son el mecanismo básico de comunicación entre rutinas. Los valores se envían al canal (con channel <- value ) y se pueden recibir en el otro lado (con value = <- channel ). Los canales son "seguros para goroutines", lo que significa que cualquier número de goroutines puede enviar y recibir de ellos al mismo tiempo.

almacenamiento en búfer

El almacenamiento en búfer de los canales de comunicación puede ser una optimización del rendimiento en determinadas circunstancias, pero debe usarse con mucho cuidado (¡y con evaluación comparativa!).

Sin embargo, hay usos para los canales almacenados en búfer que no se relacionan directamente con la comunicación.

Por ejemplo, un lenguaje de limitación común crea un canal con (por ejemplo) un tamaño de búfer "10" y luego envía diez tokens inmediatamente. Luego se genera cualquier cantidad de rutinas de trabajo, y cada una recibe un token del canal antes de comenzar a trabajar y lo envía de regreso después. Entonces, por muchos trabajadores que haya, sólo diez estarán trabajando al mismo tiempo.

De forma predeterminada, los canales de Go no tienen búfer . Esto significa que enviar un valor a un canal se bloqueará hasta que otra rutina esté lista para recibirlo de inmediato. Go también admite tamaños de búfer fijos para canales (usando make(chan someType, bufferSize) ). Sin embargo, para un uso normal, esto suele ser una mala idea .

Imagine un servidor web para nuestro fondo, donde cada solicitud hace un retiro. Cuando las cosas están muy ocupadas, FundServer no podrá mantenerse al día, y las solicitudes que intentan enviarse a su canal de comando comenzarán a bloquearse y esperar. En ese momento, podemos imponer un recuento máximo de solicitudes en el servidor y devolver un código de error sensible (como un 503 Service Unavailable ) a los clientes que superen ese límite. Este es el mejor comportamiento posible cuando el servidor está sobrecargado.

Agregar almacenamiento en búfer a nuestros canales haría que este comportamiento fuera menos determinista. Fácilmente podríamos terminar con largas colas de comandos sin procesar según la información que el cliente vio mucho antes (y tal vez para solicitudes que se habían agotado desde entonces). Lo mismo se aplica en muchas otras situaciones, como aplicar contrapresión sobre TCP cuando el receptor no puede seguir el ritmo del remitente.

En cualquier caso, para nuestro ejemplo de Go, nos quedaremos con el comportamiento predeterminado sin búfer.

Usaremos un canal para enviar comandos a nuestro FundServer . Cada trabajador de referencia enviará comandos al canal, pero solo el servidor los recibirá.

Podríamos convertir nuestro tipo de fondo en una implementación de servidor directamente, pero eso sería complicado: estaríamos mezclando el manejo de simultaneidad y la lógica comercial. En su lugar, dejaremos el tipo de Fondo exactamente como está, y haremos de FundServer un envoltorio separado a su alrededor.

Como cualquier servidor, el contenedor tendrá un bucle principal en el que espera los comandos y responde a cada uno de ellos. Hay un detalle más que debemos abordar aquí: el tipo de los comandos.

Un diagrama del fondo que se utiliza como servidor en este tutorial de programación de Go.

Punteros

Podríamos haber hecho que nuestro canal de comandos tome *punteros* a los comandos (`chan *TransactionCommand`). ¿Por qué no lo hicimos?

Pasar punteros entre gorutinas es arriesgado, porque cualquiera de las gorutinas podría modificarlo. También suele ser menos eficiente, porque la otra gorutina podría estar ejecutándose en un núcleo de CPU diferente (lo que significa más invalidación de caché).

Siempre que sea posible, prefiera pasar valores simples.

En la siguiente sección a continuación, enviaremos varios comandos diferentes, cada uno con su propio tipo de estructura. Queremos que el canal de Comandos del servidor acepte cualquiera de ellos. En un lenguaje OOP, podríamos hacer esto a través del polimorfismo: hacer que el canal tome una superclase, de la cual los tipos de comandos individuales fueran subclases. En Go, usamos interfaces en su lugar.

Una interfaz es un conjunto de firmas de métodos. Cualquier tipo que implemente todos esos métodos puede tratarse como esa interfaz (sin declararse para hacerlo). Para nuestra primera ejecución, nuestras estructuras de comando en realidad no expondrán ningún método, por lo que usaremos la interfaz vacía, interface{} . Dado que no tiene requisitos, cualquier valor (incluidos los valores primitivos como los números enteros) satisface la interfaz vacía. Esto no es ideal, solo queremos aceptar estructuras de comando, pero volveremos a eso más adelante.

Por ahora, comencemos con el andamiaje para nuestro servidor Go:

servidor.ir

 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 } }

Ahora agreguemos un par de tipos de estructura de Golang para los comandos:

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

El WithdrawCommand solo contiene la cantidad a retirar. No hay respuesta. BalanceCommand tiene una respuesta, por lo que incluye un canal para enviarla. Esto asegura que las respuestas siempre llegarán al lugar correcto, incluso si nuestro fondo luego decide responder fuera de orden.

Ahora podemos escribir el bucle principal del 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)) } } }

Mmm. Eso es un poco feo. Estamos activando el tipo de comando, usando aserciones de tipo y posiblemente fallando. Sigamos adelante de todos modos y actualicemos el punto de referencia para usar el 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) } }

Eso también fue un poco feo, especialmente cuando revisamos el saldo. No importa. Vamos a intentarlo:

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

Mucho mejor, ya no estamos perdiendo retiros. Pero el código se está volviendo difícil de leer y hay problemas más serios. Si alguna vez emitimos un BalanceCommand y luego olvidamos leer la respuesta, nuestro servidor de fondos se bloqueará para siempre al intentar enviarlo. Limpiemos un poco las cosas.

Conviértelo en un servicio

Un servidor es algo con lo que hablas. ¿Qué es un servicio? Un servicio es algo con lo que hablas con una API . En lugar de hacer que el código del cliente funcione directamente con el canal de comandos, haremos que el canal no se exporte (privado) y envolveremos los comandos disponibles en funciones.

 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 } }

Ahora nuestro punto de referencia solo puede decir server.Withdraw(1) y balance := server.Balance() , y hay menos posibilidades de enviarle accidentalmente comandos no válidos u olvidar leer las respuestas.

Así es como se vería el uso del fondo como un servicio en este programa de lenguaje Go de muestra.

Todavía hay una gran cantidad de repeticiones adicionales para los comandos, pero volveremos a eso más adelante.

Actas

Eventualmente, el dinero siempre se acaba. Pongámonos de acuerdo en que dejaremos de retirar dinero cuando nuestro fondo se haya reducido a sus últimos diez dólares, y gastemos ese dinero en una pizza comunitaria para celebrar o compadecernos. Nuestro punto de referencia reflejará esto:

 // 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) }

Esta vez sí podemos predecir el 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 regreso donde comenzamos: varios trabajadores pueden leer el saldo a la vez y luego actualizarlo. Para lidiar con esto, podríamos agregar algo de lógica en el propio fondo, como una propiedad de minimumBalance , o agregar otro comando llamado WithdrawIfOverXDollars . Ambas son ideas terribles. Nuestro acuerdo es entre nosotros, no una propiedad del fondo. Debemos mantenerlo en la lógica de la aplicación.

Lo que realmente necesitamos son transacciones , en el mismo sentido que las transacciones de bases de datos. Dado que nuestro servicio ejecuta solo un comando a la vez, esto es muy fácil. Agregaremos un comando Transact que contiene una devolución de llamada (un cierre). El servidor ejecutará esa devolución de llamada dentro de su propia gorutina, pasando el Fund sin procesar. La devolución de llamada puede hacer con seguridad lo que quiera con el Fund .

Semáforos y errores

En el siguiente ejemplo, estamos haciendo mal dos pequeñas cosas.

Primero, estamos usando un canal `Terminado` como un semáforo para que el código de llamada sepa cuándo ha terminado su transacción. Está bien, pero ¿por qué el tipo de canal es `bool`? Solo enviaremos `true` para que signifique "hecho" (¿qué significaría enviar `false`?). Lo que realmente queremos es un valor de estado único (¿un valor que no tiene valor?). En Go, podemos hacer esto usando el tipo de estructura vacía: `struct{}`. Esto también tiene la ventaja de usar menos memoria. En el ejemplo, nos quedaremos con `bool` para no dar demasiado miedo.

En segundo lugar, nuestra devolución de llamada de transacción no devuelve nada. Como veremos en un momento, podemos obtener valores de la devolución de llamada en el código de llamada usando trucos de alcance. Sin embargo, las transacciones en un sistema real probablemente fallarían a veces, por lo que la convención de Go sería que la transacción devuelva un "error" (y luego verifique si fue "nil" en el código de llamada).

Tampoco vamos a hacer eso por ahora, ya que no tenemos ningún error que generar.
 // 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 // ... } } }

Nuestras devoluciones de llamadas de transacciones no devuelven nada directamente, pero el lenguaje Go facilita la obtención directa de valores de un cierre, por lo que lo haremos en el punto de referencia para establecer el indicador pizzaTime cuando el dinero se agote:

 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 } }

Y comprueba que funciona:

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

Nada más que transacciones

Es posible que hayas visto una oportunidad de limpiar las cosas un poco más ahora. Dado que tenemos un comando Transact genérico, ya no necesitamos WithdrawCommand o BalanceCommand . Los reescribiremos en términos de transacciones:

 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) }) }

Ahora, el único comando que toma el servidor es TransactionCommand , por lo que podemos eliminar todo el lío de la interface{} en su implementación y hacer que acepte solo comandos de transacción:

 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 } }

Mucho mejor.

Hay un último paso que podríamos dar aquí. Aparte de sus funciones de conveniencia para Balance y Withdraw , la implementación del servicio ya no está ligada al Fund . En lugar de administrar un Fund , podría administrar una interface{} y usarse para envolver cualquier cosa . Sin embargo, cada devolución de llamada de transacción tendría que convertir la interface{} nuevamente a un valor real:

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

Esto es feo y propenso a errores. Lo que realmente queremos son genéricos en tiempo de compilación, por lo que podemos "plantear" un servidor para un tipo particular (como *Fund ).

Desafortunadamente, Go no es compatible con los genéricos, todavía. Se espera que llegue eventualmente, una vez que alguien descubra alguna sintaxis y semántica sensata para ello. Mientras tanto, el diseño cuidadoso de la interfaz a menudo elimina la necesidad de genéricos, y cuando no lo hacen, podemos arreglárnoslas con aserciones de tipo (que se verifican en tiempo de ejecución).

¿Terminamos?

Si.

Bueno, está bien, no.

Por ejemplo:

  • Un pánico en una transacción matará todo el servicio.

  • No hay tiempos de espera. Una transacción que nunca regresa bloqueará el servicio para siempre.

  • Si nuestro Fondo genera algunos campos nuevos y una transacción falla a la mitad de su actualización, tendremos un estado inconsistente.

  • Las transacciones pueden filtrar el objeto Fund administrado, lo cual no es bueno.

  • No existe una forma razonable de realizar transacciones en varios fondos (como retirar de uno y depositar en otro). No podemos simplemente anidar nuestras transacciones porque permitiría interbloqueos.

  • Ejecutar una transacción de forma asíncrona ahora requiere una nueva rutina y mucho juego. En relación con esto, probablemente queramos poder leer el estado del Fund más reciente desde otro lugar mientras se está realizando una transacción de larga duración.

En nuestro próximo tutorial del lenguaje de programación Go, veremos algunas formas de abordar estos problemas.

Relacionado: Lógica bien estructurada: un tutorial de Golang OOP