Язык программирования Go: вводное руководство по Golang
Опубликовано: 2022-03-11Что такое язык программирования Go?
Относительно новый язык программирования Go аккуратно расположился посередине, предоставляя множество хороших функций и намеренно опуская многие плохие. Он быстро компилируется, быстро работает, включает среду выполнения и сборку мусора, имеет простую систему статических типов и динамические интерфейсы, а также превосходную стандартную библиотеку. Вот почему так много разработчиков стремятся изучить программирование на Go.
ООП — одна из тех функций, которые Go намеренно опускает. У него нет подклассов, поэтому нет бриллиантов наследования, супервызовов или виртуальных методов, которые могли бы сбить вас с толку. Тем не менее, многие полезные части ООП доступны другими способами.
*Миксины* доступны при анонимном встраивании структур, что позволяет вызывать их методы непосредственно в содержащей их структуре (см. встраивание). Продвижение методов таким образом называется *forwarding*, и это не то же самое, что создание подклассов: метод по-прежнему будет вызываться во внутренней встроенной структуре.
Встраивание также не подразумевает полиморфизм. Хотя `A` может иметь `B`, это не означает, что это `B` - функции, которые принимают `B`, не будут принимать вместо этого `A`. Для этого нам нужны интерфейсы , с которыми мы кратко познакомимся позже.
Между тем, Golang занимает сильную позицию в отношении функций, которые могут привести к путанице и ошибкам. Он опускает идиомы ООП, такие как наследование и полиморфизм, в пользу композиции и простых интерфейсов. Он преуменьшает обработку исключений в пользу явных ошибок в возвращаемых значениях. Существует ровно один правильный способ компоновки кода Go, реализованный с помощью инструмента gofmt
. И так далее.
Зачем изучать Голанг?
Go также является отличным языком для написания параллельных программ : программ со многими независимо работающими частями. Очевидным примером является веб-сервер: каждый запрос выполняется отдельно, но запросы часто должны совместно использовать ресурсы, такие как сеансы, кэши или очереди уведомлений. Это означает, что опытные программисты на Go должны иметь дело с одновременным доступом к этим ресурсам.
Хотя в Golang есть отличный набор низкоуровневых функций для обработки параллелизма, их прямое использование может оказаться сложным. Во многих случаях несколько повторно используемых абстракций над этими низкоуровневыми механизмами значительно облегчают жизнь.
В сегодняшнем руководстве по программированию на Go мы рассмотрим одну из таких абстракций: оболочку, которая может превратить любую структуру данных в транзакционную службу . В качестве примера мы будем использовать тип Fund
— простое хранилище для оставшегося финансирования нашего стартапа, где мы можем проверить баланс и снять средства.
Чтобы продемонстрировать это на практике, мы будем строить службу небольшими шагами, создавая беспорядок по пути, а затем снова очищая его. По мере прохождения нашего руководства по Go мы столкнемся с множеством интересных функций языка Go, в том числе:
- Типы и методы структур
- Юнит-тесты и бенчмарки
- Горутины и каналы
- Интерфейсы и динамическая типизация
Создание простого фонда
Давайте напишем код для отслеживания финансирования нашего стартапа. Фонд стартует с заданным балансом, а деньги можно только вывести (выручку разберем позже).
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 секунды (по умолчанию). Чтобы гарантировать это, он будет вызывать бенчмарк несколько раз, каждый раз передавая увеличивающееся значение «числа итераций» (поле bN
), пока выполнение не займет не менее секунды.
На данный момент наш эталон просто вносит немного денег, а затем снимает их по одному доллару за раз.
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
Это прошло хорошо. Мы выполнили два миллиарда (!) итераций, и финальная проверка баланса прошла правильно. Мы можем игнорировать предупреждение «нет тестов для запуска», которое относится к модульным тестам, которые мы не писали (в более поздних примерах программирования на Go в этом руководстве предупреждение обрезается).
Параллельный доступ в 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 ()
Как только все наши горутины созданы, нам нужен способ дождаться их завершения. Мы могли бы создать его сами, используя каналы , но мы еще не сталкивались с ними, так что это было бы пропуском вперед.
На данный момент мы можем просто использовать тип WaitGroup
из стандартной библиотеки Go, которая существует именно для этой цели. Мы создадим один (назовем его « wg
») и вызовем wg.Add(1)
перед созданием каждого рабочего, чтобы отслеживать их количество. Затем рабочие отчитаются, используя wg.Done()
. Тем временем в основной горутине мы можем просто сказать wg.Wait()
для блокировки, пока все рабочие не закончат работу.
Внутри рабочих горутин в нашем следующем примере мы будем использовать defer
для вызова wg.Done()
.
defer
принимает вызов функции (или метода) и запускает его непосредственно перед возвратом из текущей функции, после того как все остальное будет сделано. Это удобно для очистки:
func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()
Таким образом, мы можем легко сопоставить Unlock
с его Lock
для удобочитаемости. Что еще более важно, отложенная функция будет работать, даже если в основной функции произойдет паника (что-то, что мы могли бы обработать с помощью try-finally в других языках).
Наконец, отложенные функции будут выполняться в порядке, обратном тому, в котором они были вызваны, а это означает, что мы можем хорошо выполнить вложенную очистку (аналогично идиоме C вложенных goto
и label
, но намного аккуратнее):
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
будет считывать баланс, вычитать единицу, а затем записывать обратно. Но иногда два или более рабочих читают один и тот же баланс и делают одно и то же вычитание, и в итоге мы получим неправильную сумму. Верно?
$ go test -bench . funding BenchmarkWithdrawals 2000000000 2.01 ns/op ok funding 4.220s
Нет, все равно проходит. Что здесь случилось?
Помните, что горутины — это зеленые потоки — ими управляет среда выполнения Go, а не ОС. Среда выполнения планирует горутины для любого количества доступных потоков ОС. Во время написания этого учебника по языку Go Go не пытается угадать, сколько потоков ОС он должен использовать, и если нам нужно более одного, мы должны об этом сказать. Наконец, текущая среда выполнения не вытесняет горутины — горутина будет продолжать работать до тех пор, пока не сделает что-то, что предполагает ее готовность к перерыву (например, взаимодействие с каналом).
Все это означает, что, хотя наш тест теперь является параллельным, он не является параллельным . Одновременно будет работать только один из наших рабочих процессов, и он будет работать до тех пор, пока не завершится. Мы можем изменить это, сказав Go использовать больше потоков через переменную среды 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
Так-то лучше. Теперь мы, очевидно, теряем часть наших изъятий, как и ожидали.
Сделать это сервером
На данный момент у нас есть различные варианты. Мы могли бы добавить явный мьютекс или блокировку чтения-записи вокруг фонда. Мы могли бы использовать сравнение и замену с номером версии. Мы могли бы сделать все возможное и использовать схему CRDT (возможно, заменив поле balance
списками транзакций для каждого клиента и вычислив баланс на их основе).
Но мы не будем делать ничего из этого сейчас, потому что это грязно или страшно, или и то, и другое. Вместо этого мы решим, что фонд должен быть сервером . Что такое сервер? Это то, с чем вы разговариваете. В Go вещи общаются через каналы.
Каналы — это основной механизм связи между горутинами. Значения отправляются в канал (с channel <- value
) и могут быть получены на другой стороне (со value = <- channel
). Каналы являются «безопасными горутинами», что означает, что любое количество горутин может отправлять и получать от них одновременно.
Буферизация каналов связи может быть оптимизацией производительности при определенных обстоятельствах, но ее следует использовать с большой осторожностью (и сравнительным анализом!).
Однако буферизованные каналы используются не только для связи.
Например, распространенная идиома регулирования создает канал с (например) размером буфера «10», а затем немедленно отправляет в него десять токенов. Затем создается любое количество рабочих горутин, и каждая из них получает токен из канала перед началом работы и отправляет его обратно после этого. Тогда, сколько бы ни было рабочих, только десять будут работать одновременно.
По умолчанию каналы Go не буферизованы . Это означает, что отправка значения в канал будет заблокирована до тех пор, пока другая горутина не будет готова принять его немедленно. Go также поддерживает фиксированные размеры буферов для каналов (используя make(chan someType, bufferSize)
). Однако для обычного использования это обычно плохая идея .
Представьте веб-сервер для нашего фонда, где каждый запрос делает вывод. Когда все очень занято, FundServer
не сможет идти в ногу, и запросы, пытающиеся отправить на его командный канал, начнут блокироваться и ждать. В этот момент мы можем установить максимальное количество запросов на сервере и вернуть разумный код ошибки (например, 503 Service Unavailable
) клиентам сверх этого ограничения. Это наилучшее возможное поведение, когда сервер перегружен.

Добавление буферизации к нашим каналам сделало бы это поведение менее детерминированным. Мы могли бы легко получить длинные очереди необработанных команд, основываясь на информации, которую клиент видел намного раньше (и, возможно, для запросов, время ожидания которых истекло в восходящем направлении). То же самое относится и ко многим другим ситуациям, таким как применение противодавления по TCP, когда получатель не может идти в ногу с отправителем.
В любом случае, для нашего примера Go мы будем придерживаться небуферизованного поведения по умолчанию.
Мы будем использовать канал для отправки команд на наш FundServer
. Каждый бенчмарк-воркер будет отправлять команды в канал, но получать их будет только сервер.
Мы могли бы превратить наш тип Fund в реализацию сервера напрямую, но это было бы беспорядочно — мы бы смешивали обработку параллелизма и бизнес-логику. Вместо этого мы оставим тип Fund таким, какой он есть, и сделаем FundServer
отдельной оболочкой вокруг него.
Как и любой сервер, оболочка будет иметь основной цикл, в котором она ожидает команды и отвечает на каждую по очереди. Здесь нужно остановиться еще на одной детали: типе команд.
Мы могли бы сделать так, чтобы наш канал команд принимал *указатели* на команды (`chan *TransactionCommand`). Почему нет?
Передача указателей между горутинами опасна, потому что любая горутина может их изменить. Это также часто менее эффективно, потому что другая горутина может работать на другом ядре ЦП (что означает большую недействительность кеша).
По возможности предпочитайте передавать простые значения.
В следующем разделе ниже мы отправим несколько разных команд, каждая со своим типом структуры. Мы хотим, чтобы канал команд сервера принимал любой из них. В языке ООП мы могли бы сделать это с помощью полиморфизма: сделать так, чтобы канал принимал суперкласс, из которого отдельные типы команд были подклассами. Вместо этого в 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()
, и меньше шансов случайно отправить ему недопустимые команды или забыть прочитать ответы.
Для команд еще много лишнего шаблона, но мы вернемся к этому позже.
Транзакции
В конце концов, деньги всегда заканчиваются. Давайте договоримся, что мы прекратим снятие средств, когда наш фонд опустеет до последних десяти долларов, и потратим эти деньги на общую пиццу, чтобы отпраздновать или посочувствовать окружающим. Наш тест будет отражать это:
// 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
все, что ему заблагорассудится.
В следующем примере мы делаем две маленькие вещи неправильно.
Во-первых, мы используем канал Done в качестве семафора, чтобы сообщить вызывающему коду, когда его транзакция завершена. Это хорошо, но почему тип канала `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 } }
Намного лучше.
Есть последний шаг, который мы могли бы сделать здесь. Помимо удобных функций для Balance
и Withdraw
, реализация сервиса больше не привязана к 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 мы рассмотрим некоторые способы решения этих проблем.