6 ms·
I agree that Go forces you to focus on mechanics of your code more so than some "fancier" languages. However, Go's poor type system also forces you to write wo
by atombender 7y ago
I agree that Go forces you to focus on mechanics of your code more so than some "fancier" languages.
However, Go's poor type system also forces you to write worse and less safe code than you want.
For example, the lack of enums and sum types means it's hard to represent, safely and efficiently, anything that should be the same type but have different values within a strict set of allowed values. The nearest you get in Go to an enum is:
type Color int
const (
Red Color = iota
Blue
Green
)
Alternatively, can do the same with a string:
type Color string
const Red Color = "red"
// etc.
This gives you some type safety, since a Color requires casting to force an int or string or whatever to become it. But that doesn't give you real safety, since you can do:
var c Color = Color("fnord")
This comes up whenever you're handling foreign input. JSON unmarshaling, for example. You can override the unmarshalling to add some validation, but it won't solve your other cases; it will always be possible to accidentally accept something bad. Not to mention that the "zero value" of such a value is usually wrong:
type Color string
var c color // Empty string, not valid!
A more safe way is to hide the value:
type Color interface {
isColor()
}
type colorValue string
func (colorValue) isColor() {}
var Red Color = colorValue("red") // etc.
Now nobody can create an invalid value, and the zero value is nil, not an invalid enum value. But this is unnecessarily complicated and shouldn't be necessary in a modern language in 2019.
The case of genuine sum types is worse. Your best bet is to use sealed interfaces:
type Action interface {
isAction()
}
type TurnLeft struct{}
func (TurnLeft) isAction() {}
type MoveForward struct{
Steps int
}
func (MoveForward) isAction() {}
There are some downsides. You have to use static analysis tools (go-sumtype is good) to make sure your type switches are exhaustive. You get a performance penalty from having to wrap all values in an interface. And if you're going to serialize or deserialize this (e.g. JSON), you will be writing a whole bunch of logic to read and write such "polymorphic" values.
- gen220 7y agoI know it’s not a proper part of the language, so it might qualify a “hack” solution, but one can get around some of the issues you describe by defining your enums in protobufs, and using/trusting the generated code, no? It’s not a pretty solution from a language design point of view, but it’s been more than effective for us from an engineering point of view: since we’d need those proto definitions anyway, why bother writing our own?
- crimsonalucard 7y agoMetaprogramming fixes everything. Why didn't they put in sum types in the first place? I think the designers probably didn't know about it at the time.
- pjmlp 7y agoGiven the linage of the designers they were pretty much aware of it.
- crimsonalucard 7y agoThe "linage" implies that they aren't aware of it, or at least they weren't when designing the language. Rob Pike is more of a systems guy than a type theorist. Like most systems guys, they know of how C handles types and how C++ handles types. The concept of Parametric Polymorphism or sum types outside of OOP is most likely something Rob Pike was not familiar with as it's hard to see why sum types were not included in the language. Having IO functions return a tuple of error and value rather then an Option type is not simplicity it's complexity arising from lack of a type primitive. The feature is ugly and very much looks like it was implemented by someone who wasn't aware of a type that can be a Value OR an Error. So instead he implemented a type that can be an Error AND a Value and left it to manual run time checks for the programmer to figure out if an error even occurred. The other thing is that this "tuple" type of error AND value looks like a hack. Tuple types can't be saved to a variable in GO and can only be returned by functions and immediately unrolled into its separate primitive values. It's like Rob knew something was off in this area so he created a temporary concept of a tuple in the return value of a function to make up for it. A consistently designed language wouldn't have a Tuple only returnable by a function call. It seems strange and inconsistent. Additionally, the fact that, in GO, some types can have Nils and other types default to a zero value implies that Rob Knew nulls were bad but didn't know how to completely get rid of the null. I'm thinking that Robs initial notions of sum types and parametric polymorphism is that they can only be implemented via hierarchies of inheritance which in itself has many problems. It makes sense because this is what systems programmers are exposed to (C, C++) as opposed to typed lambda calculus or Haskell. So it's easy to see that Go is the result of Robs awareness of problems with OOP but lack of awareness of the theoretical alternative.
- strken 7y agoAgree with this. There are couple of observations I'd make, though. Firstly, "enums" using iota should always be defined as either const ( Red Colour = iota + 1 Blue Green ) or const ( Unknown Colour = iota Red Green Blue ) to avoid the zero value problem. Secondly, and this is a personal preference, I've really enjoyed not having to work with sum types. In practice other programmers seem to use them when a public interface would have been sufficient, and it's convenient to be able to do: // original package type Position struct { X, Y int Rot Rotation } type Action interface { Apply(Position) } type MoveForward struct { Steps int } func (m MoveForward) Apply(p Position) { switch p.Rot { case Up: p.Y += m.Steps ... } } // second package wrapping the first type WarpToPoint struct { X, Y int } func (w WarpToPoint) Apply(p movement.Position) { p.X, p.Y = w.X, w.Y }
- riquito 7y agoEnums exist specifically to be compared with one of their possible values, how can you have a zero value problem?
- gen220 7y agoIf the caller of your interface does not specify a value for your enum, they have implicitly specified the zero value. Whether that’s desirable behavior or not is up to you. For most clients, this behavior can be surprising if the zero value is a meaningful one (i.e. one that implies an intentional choice). IME it’s useful to explicitly define the zero value’s meaning as “UNSPECIFIED”, to simplify the problem of trying to guess if the client intended to pass a zero value.
- reificator 7y ago> Enums exist specifically to be compared with one of their possible values, how can you have a zero value problem? Because there's not actually support for enums in go. There's support for constant values, and automatically assigning sequential values to them. That happens to be useful for solving the same kinds of problems that enums solve, but they're not equivalent.
- skybrian 7y agoIt probably amounts to the same thing, but I think there's a more pragmatic approach to thinking about safety. A type is a way to remember that validation has already been done. This is true for constants by inspection (code review). For dynamic code, have a validator function that takes unvalidated input and returns a Color or an error, and always use that to create a Color. That's usually sufficient. Any "cheating" should come up in code review as a suspicious cast to Color. In an audit, you could search for casts to Color. Safe languages often have unsafe constructs. It's the same principle. The unsafe code is signposted, and you review it. If you want further encapsulation, another useful trick is to make Color a struct with a private field. It's not usually necessary, though. Go does have an unfortunate quirk that you can always create a zero value without calling a constructor, so you'll need to make sure a zero Color has meaning. (An interface doesn't really change this because the then the zero value is nil. That's not an improvement over making the zero value mean "black" or "transparent" or "invalid".)
- weberc2 7y agoI want to see generics and real enums (I.e., Rust enums, not C/Java enums) added to Go, but as far as it being unsafe or inefficient, these concerns are overblown for many apps. People who levy this criticism are often fine with Python and/or JS for similar categories of applications, even though they are far less safe and less performant than Go. We should be clear when we criticize Go that we’re talking about addressing the last 1% or so of type-related bugs and/or extending the performance ceiling a bit higher. We should also give Go credit for permitting a high degree of safety, performance, and productivity when other languages make you choose one.
- lowmagnet 7y agotype Color struct { R, G, B, A byte // IDK } func (o OtherType) *Color { return &Color{R: o.r, B: o.b, G: o.g} } type Colorer interface { func Color() *Color } A colorer would return a color regardless of its type. This is behavior-based interface. I just need a thing that when I call Color(), you get a *Color.
- atombender 7y agoThat's not an enum or sum type, though, and misses the point of my example. For colors, sure, you can use a structural type to represent RGBA, but that wasn't what I was trying to get across. What if the set of possible values cannot be described as scalars? The other example with "actions" demonstrated this problem.
- SEJeff 7y agoFor json unmarshalling of structs at least, I've become a pretty big fan of the validator library from the go playground: github.com/go-playground/validator/v10 It uses struct tags to validate the struct and is quite extensive. https://godoc.org/gopkg.in/go-playground/validator.v10 https://godoc.org/gopkg.in/go-playground/validator.v10