Pretty much Javascript callback spaghetti in Go. There might be a way around it, but I haven't found any examples. Perhaps there could be a way where transactions a manually managed.
Pretty much Javascript callback spaghetti in Go. There might be a way around it, but I haven't found any examples. Perhaps there could be a way where transactions a manually managed.
I have observed before that this sort of DSL-like usage can sometimes make people forget that they are dealing with plain old-fashioned language constructs that can be manipulated with other constructs. Languages like Ruby that allow this fact to disappear out of the source code completely seem to make this more likely. Go won't let you forget you're just creating a bunch of closures, but if you've been trained in an enviroment where a "block" is something special or in such a Ruby "DSL" you may have a way of thinking about these issues that inhibits proper refactoring. This is the general case of what okatsu observes before me, that nothing stops you from making real functions or methods to use as the methods.
Also, Go's relatively recent "bound methods" are really useful for this sort of thing.
There are several benefits to using the wrapper functions:
1. It lets you return an error to rollback a read-write transaction. This is typically what you want. That error will get passed through as a return from DB.Update().
2. Because of #1, you can have one error check for application errors and system errors (e.g. disk full) after a transaction commits instead of an error check for DB.Begin() and DB.Commit().
3. If you forget to close a read transaction then it will keep old pages around to maintain consistency but this will cause your database to grow without bound. If you use the helper functions then you're protected from this even in the event of a panic().
The API is modeled around database/sql where applicable. Let me know if you find anything confusing and I can update the documentation to make it more clear.
Never really tried yet, but I think when using database/sql.Tx, the following snippet would be quite idiomatic, and I believe it should be perfectly safe:
tx, err := mydb.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec('...')
if err != nil {
return err
}
return tx.Commit()https://github.com/boltdb/bolt#managing-transactions-manuall...
The "defer tx.Rollback()" makes me cringe a little since it's confusing when you read the code top-down. It also throws out the error returned from Rollback(). Granted, the only possible error is if the DB is closed so it's not a big deal in most situations.
func action(...) (err error) {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("preparing transaction: %v", err)
}
defer func(tx *sql.Tx) {
if err != nil {
_ = tx.Rollback()
} else {
err = tx.Commit()
}
}(tx)
// do something
return
}If you take a look at the code, db.Begin is available and gives you access to the underlying transaction. You can use it as you want, however you'll need to be aware of catching any error, rollbacking, committing etc... all that is taken care of for you in db.Update (or db.View for read-only stuff)
This style is also consistent with the rest of the standard library, such as filepath.Walk or sort.Search.
Actually, going the "db.Begin" way would be more consistent with the rest of the standard library. Most of the Go APIs go to great length to be "external" (i.e. iterator-like), e.g. bufio.Scanner, and (of special note in this case!) database/sql.Tx! It appears to me the "internal"/callback APIs are introduced in those places where there are really compelling reasons, which justify breaking the consistency: e.g. for filepath.Walk, there's recurrence, so it's mostly giving up to the obviously K.I.S.S. way; in sort.Search, the benefit is that callback allows to evade need for generics/reflection.
https://cloud.google.com/appengine/docs/go/datastore/transac...
I remember that was a common pattern at Google when creating APIs interfacing with BigTable which also is a KV store. The premise is that you don't need to load everything into memory at once and instead do your work (for Update operations) or just pick interesting fields (for View operations) in smaller increments in your callback function.
> functions within functions, that can't really be refactored out or reused
This is functional programming but the choice of this paradigm does not impact code reuse. You can pass proper functions or methods and don't have to stick to lambdas.
Overall I think the concept is spot on.
As others points out there's nothing wrong the Bolt code, it's fine and pretty common, I just don't like it.
This is how I'd do it.