5 ms·
I'm really not a fan of the "initialize with empty value" approach. Especially when handling user input, e.g. marshalling JSON onto a struct, Go makes it imposs
by wolframhempel 5y ago
I'm really not a fan of the "initialize with empty value" approach. Especially when handling user input, e.g. marshalling JSON onto a struct, Go makes it impossible to distinguish between the user setting a value to 0 and the user not specifying the field. Instead, you end up with awkward "pointer to int" constructs to allow for nil checks. I'd love to have a "undefined" value instead.
- dilatedmind 5y agoI agree this can be awkward, especially if you let these constructs propagate through your codebase and database. However, if a string or int can be null, then all strings and ints are essentially pointers, so you've just introduced this construct everywhere. A couple things I have tried: - hope default values align with your business logic, eg an empty string isn't a valid name and 0 isn't a valid age. - for partial updates, populate the existing values before unmarshalling, then unmarshal on top. Missing fields in the json won't overwrite the existing values - unmarshall into a map[string]interface{}, which gives you the semantics you want.
- mumblemumble 5y agoI have never used Go in anger, only dabbled a bit and watched interestedly from the sidelines. So, my opinion probably isn't worth much. But here it is, anyway: it seems like this way of doing things would lead to a tendency toward messy, confusing domain models where you can't tell from types alone whether 0 or -1 or MIN_INT is a sentinel for "no value", or actually means that value, and possibly find yourself juggling multiple sentinel values for one type in the same scope. My own cranky opinion is that, in the 21st century, a static type system that can't even reliably distinguish between something and nothing isn't much of a type system at all. Say what you will about dynamic type systems, but it must be acknowledged that they do a much better job of maintaining a firm and logically consistent, if not statically verifiable, grasp of the most basic ontological distinction that the universe has to offer. There's always the option to wrap the atomic value in a struct of some sort. But, without generics, that's going to feel more like gRPC's awkward, verbose way of doing it than the ML family's Option<T> type. That approach is (mostly) tolerable in a datagram format, because I/O is going to be a hot mess no matter what you do, anyway, but I'd hate to have to do it with the types I'm using in my actual code. I'm generally fairly disdainful of the, "every language must cargo cult everything functional languages do," thing, but it is interesting to observe that generics and proper algebraic types would cover all of these use cases - and more - cleanly and without having to add a specific language feature to cover each one. Which has me wondering, if the goal of Go was to create a maximally simple and ergonomic imperative language, did they actually achieve that, or did they follow a greedy search into a local maximum?
- jerf 5y agoThis does not excuse the language necessarily, but it helps to understand what is going on. Go is like C, in that it cares deeply about allocations. It has various syntax glosses that may help it look more like a language like Python where everything is a reference, but it is not. If a function says it returns a struct and an error value, then that function is going to return things into a memory chunk sufficient to hold that struct and error value. "sizeof" is a well-defined operation in Go. Everything has a precise size the compiler uses. Slices may look dynamic, but they're actually a three-word structure for size, capacity, and pointer; the "dynamic size" happens behind that pointer. Maps may look dynamic, but they're a fixed struct as well under the hood. Channels may look like they could be dynamic, but they have fixed sizes as well for their internal components. Go is a value-based language, not a reference-based language. The same thing happens when you declare a struct value in a function. Memory for it is allocated immediately. "Initialize with empty value" is not a solution to any sort of type issue and has nothing to do with nil; "initialize with empty value" is a solution to C's uninitialized value problem. Unlike C, which simply grabs some RAM and gives it to you, and leaves it to you to figure out what to do with the garbage values in it, Go guarantees that all fresh values you receive are fully initialized to a well-defined "zero value". In Go, when you say you have a struct, you do, right then and there, and so, it needs a value. You don't have an "undefined" value, because Go is too low level for that. For Go to have an undefined value for a struct isn't something it could just bodge on, it isn't something that was just an "oversight", it would actually be a fundamental overhaul to the language. Go would have to be adjoining values to the structs you declare, making it harder for you to know how much memory they take, or it would have to completely shift to a reference-based language, or it would have to do something else like that not in keeping with the nature of the language. Rust, for instance, is doing more work than you may realize when you "Option" something. Think about what the memory representation of Option<byte> is. Without more information from the user, you can't use any value of the byte as the undefined value, because all 0-255 values are valid byte values. You have to stick it somewhere else. Rust, and some other languages, often do some magic to turn your byte into a 16-bit int instead and pack the invalidity into there. Having an array of Option<...>s is a non-trivial operation for the compiler, especially to do it maximally efficiently. Go doesn't do this kind of thing. The struct that is declared is what is in memory. I welcome you to think that's still a problem in the language, but I hope this at least makes why Go works this way more comprehensible.