8 ms·
There are a lot of decisions in Go that I find very distasteful. It's frustrating because Go comes very close to being something I'd want to use, but falls sho
by tiglionabbit 10y ago
There are a lot of decisions in Go that I find very distasteful. It's frustrating because Go comes very close to being something I'd want to use, but falls short in all areas.
It has this great automatic interface system! Amazing! Except it also has "primitive" types that can't participate, so many of my functions have to take interface{} instead which is pretty much void*.
It can dispatch to different functions based on the argument types! It will even automatically switch to the non-pointer type if you pass a pointer to a non-pointer function. However, you can't write a pointer and non-pointer version of the same function. Also why are we using pointers to represent reference mutability and optionality these days? Other languages have maybe/option which handles optionality much better.
Speaking of not having maybe/option, every other line of code is "if (err == nil)" and there's no way to abstract this out. Give me `option` dammit. Exception handling exists via panic, but it's a little different than you're used to and the language designers like to pretend it's not exception handling at all.
There are built-in functions that use generics, but I'm not allowed to write one? Hypocrites.
You have to name things starting with a capital letter to make them public. Who came up with this convention? It is not shared by any language ever. It also gets extremely annoying when you want to serialize json, serialize a protocol buffer, or do a database query. Name your struct members in lowercase and those libraries won't be able to see them, but name them in uppercase and the output will be in the wrong case. Instead, you have to "annotate" those struct fields repeatedly for every library you're going to pass them to. You also have to manage "naming convention" systems that translate between the two names, and these conventions are never the same between two libraries.
- wyager 10y ago> It has Structural Typing! Amazing! I wouldn't say Go has structural typing. It has a form of interfaces, which are not the same. Structural typing is when the data members of an object are expressed in its type. For example, `x : {f : t}` is a statement that the object `x` has a member called `f` of type `t`. That is structural typing. On the other hand, what Go has is pretty much the definition of interfaces, where you have constraint types that indicate that anything satisfying the constraint has some functions defined on it.
- tiglionabbit 10y agoAh, ok. I'll change my comment. I've heard it referred to as structural typing though, and googling "structural typing vs duck typing" gets the right results.
- leshow 10y agoThis isn't the same concept as structural typing I don't think, but purescript has something called extensible records that quite similar to what the previous poster describes: function :: forall a. { f :: String, g :: String | a } -> String This is a function that takes an extensible record. i.e. 'a' is some type which has fields f of type String and g of type String, at minimum.
- wyager 10y agoExtensible records are a form of structural typing. Purescript has good support for both nominal (via data declarations) and structural (via extensible records) typing. It has the best native support for both paradigms of any language that I can think of off the top of my head.
- mseepgood 10y agofunc example(x interface{f() string; g() string}) string { } Go's structural typing is based on methods instead of fields, but it's structural typing as well.
- Kalium 10y ago> It also gets extremely annoying when you want to serialize json, serialize a protocol buffer, or do a database query. This in particular is maddening when learning go. I lost hours to this when deserializing JSON.
- kevinmgranger 10y agoCalling a method defined with a pointer receiver on a non-pointer works: https://play.golang.org/p/VDkz9H9mBF https://play.golang.org/p/VDkz9H9mBF
- tiglionabbit 10y agoOh, is that new? I remember this biting me when I wanted to write a function that would interpret the value I passed to it, and have a special case for null pointers. I wanted to also be able to pass a non-pointer to this same function name and get the standard case, but the dispatch would not let me do this unless I gave both versions of the function different names.
- weberc2 10y agoThe gotcha is usually when you're trying to use that type to satisfy an interface. If the methods that satisfy an interface are defined on a pointer, then you need to pass a pointer and not a value.
- whatnotests 10y ago> You have to name things starting with a capital letter to make them public. Who came up with this convention? It is not shared by any language ever. THIS. One thing that definitely bothers many people, myself included.
- dx034 10y agoI must say I really like it. The only point where it can be very annoying is for serialization. For all other situations, I think it's much cleaner and faster to read than always adding keywords.
- Jabbles 10y agoevery other line of code is "if (err == null)" roughly how much Go would you say you've written...?
- tiglionabbit 10y agoSorry, it's nil, isn't it. It's been a year since I touched Go. I wrote enough to get a production component out using it. I mostly used it to fill in the gaps where Python wasn't all that good at being a reactor. 2k lines tops.
- shadowmint 10y ago25kloc or so over 4 years maybe? Ive never really counted. you? What point are you making? If someone writes go for an hour they'll see this pattern. Error handling in go is more verbose than exception based error handling in some other languages. /shrug I prefer it, but it's true. It's obvious the point being made here, regardless of the wrong null/nil thing.
- dx034 10y agoIt's nil and not null, and the pattern you'll see much more often (in my experience) is if res, err = func(); err != nil {}. Checking for an error to be nil seems unusual, as you'd continue with your code in that situation. And the code isn't that long, don't think it really bloats up the code.
- shadowmint 10y agoWhat ever; irrelevant semantics. The point being made was go has verbose error handling. It does. So does rust; we don't need to dance around it. Better is a different question, but that wasn't the point the OP was making.
- dx034 10y agoI didn't want to nick-pick here, OP could've just said that. It's the same as not having a ternary operator. Go is no language that allows expressing code in few lines, but by that they keep it simple to read. Even though I doubt that proper exception handling is shorter than the Go version. Many developers just choose to not do that and handle all Exceptions the same, or using try-catch blocks that stretch over hundreds of lines. It's indeed faster then, but not a good comparison.
- Analemma_ 10y ago> Speaking of not having maybe/option, every other line of code is "if (err == null)" and there's no way to abstract this out. Give me `option` dammit. This is why I can't take Go apologists seriously when they say the language is just fine without generics. Ok, fine, maybe I don't need generics in my application code as much as I thought, but their absence has cascading effects throughout the language and requires you to do ridiculous things all over the place. Not just the constant "if (err == null)" and interface{}, but also go-generate, which I have seen Go defenders seriously try to claim obviates the need for generics. What?!
- weberc2 10y agoThere are definitely legitimate use cases for generics, but `if err != nil` isn't one of them. You'll simply be pattern matching on an option type instead. Go generate is definitely not a substitute for generics.
- tiglionabbit 10y ago> You'll simply be pattern matching on an option type instead. If you have multiple things that can fail in sequence, you can "lift" into option and then the result of your expression will either be a real result or a short-circuited error. https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch8.html#pure-error-handling https://drboolean.gitbooks.io/mostly-adequate-guide/content/... Or just use exceptions. Either would be preferable to C-style error handling.
- majewsky 10y ago> If you have multiple things that can fail in sequence, you can "lift" into option Hey, look, a monad! \o/
- parenthephobia 10y agoAlthough I prefer exceptions, I like the way Rust approaches not having them. It also communicates exceptions via option types, and - by convention - functions that can go wrong return an instance of Result<SuccessType, ErrorType> initialized either with a success value or an error value. There are some helper methods on Result. I like "expect", which panics if an error is indicated and returns the success value otherwise. The real boilerplate removal comes via try!, which returns the result (actually it re-wraps it in a new Result) if it's an error. When used on functions that can fail inside functions which return Result, the result is that try! bubbles the exception up the stack. The experience is very like traditional exception handling, but you always know exactly how control can flow, since try! only returns from the function it is directly inside, and can only from functions which advertise that they can fail by having Result<> as their return type.
- weberc2 10y agoAnother poster already pointed out that you can call methods on value types, but you can also satisfy interfaces with primitive types. The bulk of your post is dogma, ironically.
- tiglionabbit 10y agoIt's not dogma. This was my experience using Go. I haven't touched go in a while though. Did they fix the interface{} problem? Specifically, I ran into this when I was using a database library. Values that can be converted to a database-friendly format implement sql.Valuer. However, the library functions just took interface{} instead of sql.Valuer because primitive types could not implement sql.Valuer. Can they implement it now? Btw, you don't get a free pass at my other objections just because someone poked a hole in one of them. The other problems still stand. Take them one by one.
- kjksf 10y agoThey can't for a very good reason. Let's say library A decides to add func (i int) MyMethod() and library B decides to add func (i int) MyMethod(). Then library C calls i.MyMethod(). Which MyMethod is executed? You can come up with an answer to that question (e.g. "pick random") but there is no sensible answer. To avoid this ambiguity you can only add methods to your own types but not someone else's (i.e. not to types defined in other packages or basic types).
- weberc2 10y agoThere never was an interface problem. The sql libraries opt to accept an `interface{}` instead of `sql.Valuer` as a matter of convenience (so you don't have to define the requisite methods). It wasn't a technical limitation. > Btw, you don't get a free pass at my other objections just because someone poked a hole in one of them. The other problems still stand. Take them one by one. You didn't mention other problems, you just whined about Go's feature set. You didn't support your opinions with anything debatable, so there's nothing to discuss. > the language designers like to pretend it's not exception handling at all. > You have to name things starting with a capital letter to make them public. Who came up with this convention? It is not shared by any language ever. > There are built-in functions that use generics, but I'm not allowed to write one? Hypocrites.
- majewsky 10y ago> Except it also has "primitive" types that can't participate, so many of my functions have to take interface{} instead which is pretty much void*. Can you elaborate? A primitive value can easily implement an interface if you assign it additional meaning through a type: # interface Stringer { String() string } func foo(value fmt.Stringer) { fmt.Println(value.String()) } foo(1.0) # fails: int is missing method String() type ValueMeters float64 func (v ValueMeters) String() string { return fmt.Sprintf("%g meters", float64(v)) } foo(ValueMeters(1.0)) # okay, prints "1.0 meters"
- tiglionabbit 10y agoThe specific case I ran into was using a database library. The function that would compose a database query with custom parameters took interface{} as the parameter type instead of sql.Valuer. I thought this was a mistake, but when I asked the library's author, they said they had to make it take interface{} because primitive types cannot implement sql.Valuer. Nothing in your comment indicates this is not still the case. You can't implement methods on primitive types, can you? That's a shame.
- majewsky 10y agoThat's correct, and part of the larger notion that you cannot add methods to types from another module. (Primitive types come from the same pseudo-module that declares append, len, etc.) Think about it. How would you cleanly implement this? If you allow e.g. func (x int) String() string { return strconv.Itoa(x) } Then what does the compiler do when two modules declare that method? You could forbid it, but this will lead to situations where existing application code breaks because a library is updated and the new library version includes a method that was already implemented (possibly differently) by the application. The other option is to allow implicit casts such as func foo(s Stringer) { fmt.Println(s.String()) } foo(1) # implicitly casts to ValueMeters to satisfy Stringer interface But then you run into the same problem. How would you select (forward-compatibly!) the right interface implementation to cast to? EDIT: The only solution that I can see is to allow function overloads: func foo(s Stringer) { ... } func foo(x int) { ... } Without having thought it through very far, I think that this can be done consistently (precisely because all types are final in Go once the module is cmplete), but it would probably still lead to some surprising behavior. Actually, I just now see a problem with this: func foo(x int) { ... } func foo(x uint) { ... } foo(0) # which one to use? Literals don't have a type in Go until they're used in a place that requires a specific type. To make this example even more interesting, consider this: func foo(x int) uint { ... } func foo(x uint) int { ... } fmt.Printf("%t", foo(0)) # prints "uint" or "int", but which?
- owaislone 10y ago> You have to name things starting with a capital letter to make them public. Who came up with this convention? It is not shared by any language ever. One of Go's best decisions ever IMO. To each their own :)
- tiglionabbit 10y agoIt's a great decision if you don't want me to use your language. I'm already a little fed up with the camelCase vs snake_case coding style conventions I'm supposed to switch between to write an app that uses both Python and JavaScript. I don't need a third convention, especially not one that's enforced by the language and contrary to every other language in existence, as well as my serialization formats.
- deleted 10y ago[deleted]
- owaislone 10y agoWhat would be the alternative? A keyword? `public`? `export`? May be you prefer but a lot of people prefer simple a simple convention over one more keyword.
- tiglionabbit 10y agoI would prefer keywords because then if you change your mind about what should be public you don't have to hunt around for every reference to it and change its name. Or, give me a convention where public stuff looks normal and private stuff looks weird, because then at least my JSON serializer will work. In Python you lead with an underscore to denote privateness. You could use interfaces to communicate privateness. If something isn't exposed in an interface, it can be assumed to be private. Or just don't have your compiler enforce privateness. You don't need it. Plenty of languages get along fine without it.
- deleted 10y ago[deleted]
- clusmore 10y ago>It has this great automatic interface system! I like post hoc polymorphism -- there's nothing more frustrating than knowing that a class has all the functionality required to meet an interface but because the author didn't declare it as such it's unusuable. Implicit interface implementation is great when you happen to implement the right method names with the right signature perhaps by luck, or you define the interface based on the implementation but this is backwards. The problem is that if you want to implement two interfaces that share a method name or you simply named a method wrong, you're out of luck. Personally I think Haskell has the best implementation of this I've yet seen in a programming language. 1. Types (structs) are defined in one place and contain only data. 2. Typeclasses (interfaces) are defined in another place and define a collection of functions (methods) and values. 3. Functions and values are defined in another place and operate on types. 4. In yet another place you can arbitrarily pair any type with any typeclass by specifying the mapping of functions/values that you've implemented with those required by the typeclass. All four of these can be defined in separate modules, giving you the ability to allow any type (even ones you didn't define) to implement any typeclass (again, even ones you didn't define) using any implementation (even ones you didn't define) -- the implementation definition is separate from all three. Because the functions/values in tyepclasses are namespaced, there's no fear of name collision. The implementation of a typeclass by a type creates a static vtable in the compiler only, so there's no runtime penalty or dynamic dispatch. I would kill for this level of flexibility in an OOP language.
- dllthomas 10y agoI agree that Haskell currently does it best (along with Rust, which IIRC does it identically). That said, there are two ways in which the situation is not perfect. First, the dynamics you describe mean I can always add (which is great! that's usually what I need to do to get my job done). They don't help much in reorganizing. If I come out with a new package that introduces a typeclass, I'll usually provide instances for common types. If, down the line, we want to move those instances next to their definitions, both my library and theirs need breaking changes. Second, related, if I want to provide instances for types that are provided by heavy-weight dependencies, other packages that might want to depend on me would have to depend on those dependencies. Mutatis mutandis for if I am introducing the types and the libraries have the typeclasses. Peeling instances out into separate packages means a multiplication of packages or retains a coupling of dependencies, and raises maintainability concerns around orphan instances. IIRC, Backpack might fix this.
- stcredzero 10y agoIt's frustrating because Go comes very close to being something I'd want to use, but falls short in all areas. With designing APIs, that reaction is used as a proxy for "doing it right." You have to name things starting with a capital letter to make them public. Who came up with this convention? It is not shared by any language ever. Some Smalltalks used this convention with Globals.
- paulddraper 10y ago> It is not shared by any language ever. Actually, Microsoft created a languages like C#, F#, Visual Basic that promote -- though do not enforce -- this convention > Do use Pascal casing for all public member, type, and namespace names consisting of multiple words. https://msdn.microsoft.com/en-us/library/ms229043.aspx https://msdn.microsoft.com/en-us/library/ms229043.aspx C++ often has capitalization for methods (which often happen to be public) but not member (which often happen to be private). Capitalization in Ruby denotes a constant, enforced by the runtime. Java uses ALL_CAPS for public static constants and lowercase for private ones. Capitalizing public things is not that weird.