8 ms·
Here's an example of why Go's simplicity is complicated: Say I want to take a uuid.UUID [1] and use it as my id type for some database structs. At first, I ju
by whateveracct 7y ago
Here's an example of why Go's simplicity is complicated:
Say I want to take a uuid.UUID [1] and use it as my id type for some database structs.
At first, I just use naked UUIDs as the struct field types, but as my project grows, I find that it would be nice to give them all unique types to both avoid mixups and to make all my query functions clearer as to which id they are using.
type DogId uuid.UUID
type CatId uuid.UUID
I go to run my tests (thank goodness I have tests for my queries) and everything breaks! Postgres is complaining that I'm trying to use bytes as a UUID. What gives? When I remove the type definition and use naked UUIDs, it works fine!
The issue is Go encourages reflection for this use-case. The Scan() and Value() methods of a type tell the sql driver how to (de)serialize the type. uuid.UUID has those methods, but when I use a type definition around UUID, it loses those methods.
So the correct way to wrap a UUID to use in your DB is this:
type DogId struct { uuid.UUID }
type CatId struct { uuid.UUID }
Go promised me that I wouldn't have to deal with such weird specific knowledge of its semantics. But alas I always do.
[1] https://github.com/google/uuid https://github.com/google/uuid
EDIT: This issue also affects encoding/json. You can see it in this playground for yourself! https://play.golang.org/p/erfcSIe-Z7b https://play.golang.org/p/erfcSIe-Z7b
EDIT: I wrongly used type aliases in the original example, but my issue is with type definitions (`type X Y` instead of `type X = Y`). So all you commenters saying that I did the wrong thing, have another look!
- skrtskrt 7y agoI'm not sure, I kind of like this? Personally I would never create different UUID types for each DB struct, and have never seen that done. type DogId struct { uuid.UUID } is type composition (Sorry if I'm not using the right term there). Isn't this exactly how you're supposed to do what you're trying to do in Go?
- hajile 7y agoIs there even a single go abstraction that doesn't leak it's guts everywhere?
- thedance 7y agoThere aren't any abstractions in any language or library that don't leak everything about what they are trying to hide as well as everything about their own implementation. That's just life. It's impossible to hide complexity. Whatever wraps one thing will be strictly more complex than the wrapped thing was.
- whateveracct 7y agoThis just isn't true! Haskell's json/sql marshalling do not use runtime reflection but instead ad hoc polymorphism, so when I create (or even derive automatically!) a marshalling instance, it is pretty easy for me to reason about what will happen statically. Haskell's Generic & newtype-deriving go a long way here, and are good examples of principled abstractions that do not leak. Haskell's conduit (and other streaming libraries) is another good example. I use them to create programs that process things in constant memory, and when I compose them (e.g. with operators like =$= or .| in conduit), the resulting program streams in constant memory. I have built entire systems (CLIs, batch jobs, event processors, etc) on top of this abstraction and conduit itself has never leaked.
- wronghorse 7y agoI believe Rust's serde library is similar when it comes to JSON. It also gives you the option to choose how strictly you want to enforce the types.
- pmarreck 7y agoThis is funny because after almost 4 years of working with Elixir and watching Go from arms’ length, I literally see NONE of the criticisms regularly leveled at Go. This is not an exaggeration. In fact, Elixir has few criticisms at all to begin with, and it’s driven very large sites already at this point. (Yeah, it can’t compile easily distributable self contained binaries. It’s not (yet) designed for that.) Not intending to start a language war, but your argument is basically the beaten wife claiming this is just standard treatment from husbands. This is my counterexample.
- throwaway894345 7y agoI agree that the standard library database tooling is really clumsy in a lot of cases, but it's the library implementation at fault, not Go itself. Notably, contrary to your last sentence, you aren't troubling yourself with "weird Go semantics", you're troubling yourself with the semantics of the database stdlib.
- whateveracct 7y agoIs there a database library that uses reflection that properly descends into type aliases? Probably not, because it isn't always what you want. It's still fundamentally caused by Go's shitty design choices. encoding/json is at fault as well, which is also in the stdlib and a flagship library (basically part of the language - the maintainers wouldn't even extend its struct tag parsing to allow for required fields it's so fundamental!) Proof that this issue affects JSON: https://play.golang.org/p/erfcSIe-Z7b https://play.golang.org/p/erfcSIe-Z7b
- gwd 7y ago> It's still fundamentally caused by Go's shitty design choices. I mean, come on. In your playground example, one way of using the UUID type inherits its methods and another doesn't. Inheritance is inherently complicated, and if you're relying on it you need to know what you're doing, no matter what language you're using. I wouldn't call that a poor design choice.
- whateveracct 7y agonewtype NotRobPike'sUUID = NotRobPike'sUUID UUID deriving newtype (ToJSON, FromJSON) ^ not inherently complicated
- throwaway894345 7y agoI'm not sure you're making the point you think you are.
- BarkMore 7y agoA type alias binds a new name to a type. All of the type’s methods are available through the new name.
- whateveracct 7y agoI fixed my comment. I didn't mean to have equal signs in there. Proof that `type X Y` causes the issue: https://play.golang.org/p/erfcSIe-Z7b https://play.golang.org/p/erfcSIe-Z7b
- BarkMore 7y agoThe type definition `type X Y` declares new type X with the underlying type of Y. X and Y share underlying types and nothing else. This is a useful feature and there's nothing weird or special case about how this works. It's just not the aliasing feature you expected.
- whateveracct 7y agoIt's still shitty and weird behavior. Not simple at all.
- sagichmal 7y agoType aliases (type X = Y) are a niche feature and not meant for this use case. If you had done e.g. type X Y you would have had more success.
- whateveracct 7y agoSorry I miswrote. I meant `type X Y` by "type alias" (terminology from other languages) `type X Y` is still affected - you will 100% _not_ have more success :) Proof (using JSON this time): https://play.golang.org/p/erfcSIe-Z7b https://play.golang.org/p/erfcSIe-Z7b
- masklinn 7y ago> `type X Y` is still affected - you will 100% _not_ have more success :) type X Y neither inherits methods nor forwards anything (what struct embedding does), so you need to know about the protocol (which you're not told because it decides to just encode the bytes), and then you need to implement its methods on the newtype explicitly forwarding them to the underlying one. TBF this is a pretty awful case as Go just goes and generates garbage without prompting.
- masklinn 7y agoI think the OP simply wrote them the wrong way around, because `type X = Y` would not have broken anything as it'd make X a trivial alias to Y, and thus would not be losing anything. Even the reflected name doesn't change from the original. type X Y however does create an entirely new type which is physically identical to the original but logically unrelated (without any of the methods or anything). Which is fine in the sense that it's what OP was looking for (completely independent types) but less so in that it doesn't forward any method, and it's not necessarily clear how you'd do that (type conversion, which looks really shitty when it involves pointers)
- uhoh-itsmaciek 7y agoNot to mention that sql.Result (the return value of Exec) has a LastInsertId() that's an int64, so if you're using uuids, you can't use that at all and have to call Query instead and manage generated IDs yourself.
- Cthulhu_ 7y agoIn a lot of cases - esp. distributed systems - it's not up to a database to generate a UUID, but the application. In theory you can have a hundred servers that generate records and send them to a central storage platform (which may or may not be a database, or event bus, etc). UUIDs are not meant to be generated by databases.
- whateveracct 7y agoHaha yes I've long since resigned myself to using Query + explicit RETURNING for inserts Cut 2 of 1000
- HelloNurse 7y agoThis is a more ridiculous symptom of bad library design than the filesystem trouble mentioned by the article. In the real world, executing most SQL statement could be made to return a semi-useful integer according to simple and consistent rules (e.g. affected row count, -1 if there's no meaningful integer). But the official Go documentation https://golang.org/pkg/database/sql/#Result https://golang.org/pkg/database/sql/#Result makes it quite clear that the Go design committee decided to imitate a remarkably limited and inelegant MySQL function that returns the value of an auto-increment column, not even realizing that only a few statements have auto-increment columns to begin with. I'd call this a negative amount of design effort. LastInsertId returns the integer generated by the database in response to a command. Typically this will be from an "auto increment" column when inserting a new row. Not all databases support this feature, and the syntax of such statements varies. (Of course, MySQL's LAST_INSERT_ID() is only bad as a building block and inspiration for a general API; in SQL queries assumptions aren't a problem and overspecialized tools can be occasionally very useful)
- apta 7y agogolang's broken interface design also encourages hacks (which lead to difficult to track down bugs) like this: https://github.com/golang/go/issues/16474 https://github.com/golang/go/issues/16474
- AnimalMuppet 7y ago> Go promised me that I wouldn't have to deal with such weird specific knowledge of its semantics. Where, specifically, did Go promise you that? I know of no languages where you don't, sooner or later, have to have weird specific knowledge of the semantics.
- whateveracct 7y agoIn all of its marketing and propaganda? Constant comparisons to languages like Java bemoaning boilerplate and idiosyncrasies? I think it's perfectly fair to say that this behavior is not in line with Go's philosophy.
- anonymoushn 7y agoOne company asked me to write a wrapper around database/sql that adds a connection pool. This is pretty easy until you want to implement any function that returns Row, which you just can't, because you can't make one of those. Amazing.
- jimsmart 7y agoTake care, you may be wrapping your connection pool around a built-in connection pool. Furthermore, some dbs might not like log-lived connections. http://go-database-sql.org/connection-pool.html http://go-database-sql.org/connection-pool.html
- anonymoushn 7y agoSorry, my original reply was unclear. The connections would be to different replicas, and it was just a hiring task rather than something one would actually use in production.