However, the issue this brings up about structs / types not explicitly declaring which interfaces they implement is a real and unaddressed problem, especially in large codebases. The only tool that I'm aware of that finds implementations is GoLand, at steep JetBrains prices.
Figuring out what type an API is asking for should not require reading every line of code in the package, and slows down every developer of large Go projects
Over three years that's like $150-$200 total and it will save you so many headaches. But that's a steep price? Are you kidding? Why do developers hate tools that cost money when they save them time and allow them to do more?
A language that depends too much on IDE integration for usability is a real problem, because now you have tool fragmentation as everyone goes different routes with varying levels of success to fix the deficiencies in your language. In the end you end up rolling your own tools as I have done, which is the absolute WORST of all worlds.
Go was supposed to be simple, but all it succeeded in doing is shifting the complexity elsewhere and calling mission accomplished. When you're designing a language, it's VERY important to understand the difference between the emergent complexity of the domain, and the inherent complexity of your design. The latter can be fixed, the former can only be managed - in ways that are already well researched (or just swept under the rug, as go has done).
Too much magic and too much "clever" re-purposing of existing paradigms (file names, capitalization, implicit contracts, etc) makes for an infuriatingly bad design.
I like working with Free and Open software much more than proprietary software. I think it's important for society, and I have more fun that way too!
Also the payoff for me has been very good, I can learn emacs once and enjoy using it for the rest of my life for all significant written language tasks on a computer.
Perhaps I could be a little more efficient if I were using a jetbrains IDE, but then I wouldn't like what I was doing as much. Enjoying what I do, even if it may look slightly contrived to others, is important in me achieving results at work.
But I'm not sure any IDE is worth $90/year when VS Code is free. The extensions for VS Code are next-level, especially the SSH extension. No other IDE comes remotely close to how well that extension works for its use case.
You can do this with a line of code below the struct definition, something like:
var _ <interface> = &<struct>{}
The compiler will also generate helpful errors if the struct doesn't implement the interface.Not requiring struct definition is a great feature in golang. I'm able to add an interface to a struct defined in another library to inject a different implementation for unit tests.
No, it's not. It's one of the best features, and I wish every language did this. At least TypeScript does it, too.
When would you want to look for all implementations of an interface? Is this something like an abstract syntax tree?
I’ve developed large Go codebases and never had this problem so your last sentence is false. In addition this is not an issue other go developers I’ve spoken to have ever worried or talked about.
Isn't it a feature? You can have a "writer" as long as it can "write", and then use that writer anywhere where a function expects a writer?
So are you trying to find implemented interfaces or interfaces' implementors?
Former: In Vim I can use `:GoImplements`, which internally calls `guru` I guess.
Latter: `gopls` supports this.
I agree it's still a pain that one can not tell directly from code what interfaces a struct implements tho.
Alan Donovan's guru tool could do this, but it kind of broke with modules and it was never updated and deprecated in favour of gopls. I don't know if gopls added this yet (I never really found a use for it).
I don't think it's very hard to write a tool for this though; parsing Go code is fairly easy and the stdlib provides a decent API for it. I think you could have a functional tool in a day if you wanted to, although without any caching it might be a little bit slow on larger code bases.
type Bar interface {
BarMethod(int, int) int
}
type Foo struct {}
// Error: Foo does not implement Bar (missing method BarMethod)
var _fooImplementsBar Bar = Foo{}Firstly You can literally just use one of the dozens of go lsps or code tools to search for API invocations to find what structs are passed/called into it. More importantly if you need to know you've written bad code. The entire point of an interface is that you SHOULDN'T need to know the underlying type. If you do you've violated the entire point. Just pass concrete ones. I've written Go for years and never had a problem with this, even in large open source projects like Kuberenetes.
Secondly, the criticism about flipping return values order/meaning isn't a criticism of interface being structurally typed (https://en.wikipedia.org/wiki/Structural_type_system). If you return int, int and the second int "should be even", you should have defined a type "Even" and returned int, Even*. Systems which are structurally typed can demonstrate functional extensionality and (https://github.com/FStarLang/FStar/wiki/SMT-Equality-and-Ext...) and check whether you've flipped the arguments, which would be a more valid criticism (but such checks are expensive and conflict with compile time requirements). Also Java has the same problem, if you define two interfaces with the same method signature and a single class implements both you can't disambiguate.
Thirdly, the structural typing has a huge advantage, namely looser coupling and more tightly defined interfaces. If you follow the "accept interfaces return structs" go idiom, you'll see why. An open source library that does so leaves their returned structs open to be used by consumer code, that itself uses interfaces, without modification required. This means most go code has small, tightly defined interfaces, where every function on the interface is invoked in the relevant function.
For example if you have a library with this definition:
type Baz struct {}
func (b Baz) Foo(){} func (b Baz) Bar(){}
I can use Baz in my code like so:
type Fooer interface { Foo() }
func DoSomething(f Fooer) { }
And use the underlying library, while being decoupled from it, without having to modify it.
Fourthly: You can explicitly say a type implements an interface...
* A good study: https://blog.boot.dev/golang/golang-interfaces/
* In Idris we would do:
even : Nat -> Bool even Z = True even (S k) = odd k where odd Z = False odd (S k) = even k
int -> even doubler a = 2 * a
To be fair, most APIs need documentation and code is just plainly not enough. At least if we are talking about specialist interfaces that aren't just another web framework.
2019, 296 comments - https://news.ycombinator.com/item?id=20166806
2018, 148 comments - https://news.ycombinator.com/item?id=16414098
2016, 47 comments - https://news.ycombinator.com/item?id=12356823
Agreed! https://pkg.go.dev/sort#Slice is wonderful. (Added a bit after this article was written, I think.)
sort.Slice(people, func(i, j int) bool { return people[i].Name < people[j].Name })
In Python one might write: people.sort(key=lambda person: person.name)
Or in Rust: people.sort_by_key(|person| person.name); // sort_by is also an option...
I think it's worth calling out exactly what is happening in the Go example:- We create a closure that captures the people slice
- We pass the people slice and the closure to the Slice function
- The Slice function mutates the people slice, and because the closure captured the slice it sees these mutations too
I get why the Go team wrote sort.Slice like that, and it was perhaps the best they could have done with the language features...But I think we're going to have to agree to disagree on how wonderful it is compared to other languages ;).
I’m looking forward to generics improving this too. (e.g. https://github.com/golang/go/issues/47619#issuecomment-91542...)
If I need to reverse the order, it looks easier to do with Go (just reverse the operator) than with Python and Rust way (I guess both have something like an "order" additional parameter).
Rust and Python both feel more elegant but I actually like Go's way.
Sorting by key is a special case (admittedly the most common special case).
1. Probably a matter of taste, but I love this feature, just because of the lack of noisy public/private keywords everywhere that you see in Java et al. It also means you can tell from a usage (not just the definition) that something is exported, which is often useful.
As far as renaming goes, either rename the definition and see where the compiler complains, or get your IDE to do it (I use GoLand, but good things are said about gopls).
As for his example, the idiomatic way to write that is either just call it `usr` or `adminUser`, or use `user := &user{}` which is valid (if a little confusing).
2. This is a feature: it allows you to define interfaces only where you need them (on the consumer side), and you define the interface with only the methods you actually need. This means that when you add a bunch of new methods on the implementation, you don't need to change all the consumers. Go interfaces are amazing.
The downside he discusses almost never happens: it's surprising, but even in large projects I've never had structs accidentally implementing interfaces or a IsAdmin method being implemented with reversed polarity by accident.
3. Definitely has its downsides. Tooling helps find unchecked errors. Though I've found the biggest downside to explicit errors is the verbosity. You do get used to it, and the explicitness is at least clear.
4. There are a couple of "magical" things like this, but they're well known and documented, and simple to fix if you run into them. I love the fact I can just name a file foo_test.go and add TestFoo methods, and "go test" finds them automatically.
5. I have not found this to be the case, and in the rare cases it does happen, the compiler tells you loudly and it's easy to fix.
6. Yeah, this is a slight pain, but the semi-official "imports" package (golang.org/x/tools/imports) fixes it up, so you just run generated code through that (and it auto-formats the code as well). It's a couple of lines of code. See: https://github.com/benhoyt/prig/blob/2df1b65a2bdf34c10bb5e57...
7. Yeah, I wouldn't mind a ternary operator. Easily misused, which is why they didn't add it, but it would be really nice used judiciously, rather than the 4-line if-else block.
8. Fixed by sort.Slice, which avoids the need for Len and Swap (and even more so by the new generics "slices" package, coming soon). I guess this was added after the article was written?
9. Fixed by "Go modules", which is really well designed and works well (though opinions differ).
10. Fixed with generics being added in Go 1.18. And generic helpers like "slices" and "maps" packages coming soon.
11. Yeah, slightly annoying for newbies, though as he mentioned, tooling tells you. I do like the control you (can) get over allocation and memory management with Go slices.
As far as his summary goes (eg: the type system getting in your way for large programs), I have definitely not found that to be the case. The author doesn't like Go, and that's okay! I don't like Java. :-)
I don't know anything about Go, but I've spent a lot of time working in a lot of languages. Structural typing is a bad idea. There are a few, limited cases where it's necessary, e.g. we have it in TypeScript because TS ultimately has to work within the limitations of JS. But if you don't have those sorts of limitations, intentionally implementing structural typing in your language is borderline negligent.
Interfaces don't just define a bag of method signatures. Interfaces are semantic, too. An Employee, a Gun, and a ClayPot might all have a method named `fire()`, but they all mean different things. But with structural typing, we can load our Employees into a Kiln and give HR a Gun to downsize the company, and nothing will stop us.
The downside of nominal interfaces seems worse: you have to import the interface which can lead to awkward situations like cyclic dependencies and type-only packages (a la Haskell). In practice, the extra friction of nominal interfaces also seems to encourage wide interfaces in contrast to Go's narrow interfaces.
If I want to change the contract of get_kittens so that it returns a set instead of a list, I found it quite tiresome to then go to all the call sites of get_kittens and change their types to match.
What was I doing wrong?* Perhaps there’s a cleverer tool out there that can infer and implement these type changes for me, automatically?
* using vim? [joke]
1. Use `:=` whenever you can, so types are inferred by the assignments/allocations 2. Alternatively query all references from `gopls` and put the results into quick fix list, then `:cdo`
My take is that Go is basically the new Java, with fewer abstractions and faster compilation. Although, the pre-Java 8 Java, before Java started to get a bit functional.
Like Java it’s a practical, imperative, statically typed, garbage collected language with very good performance. Also like (pre-Java 8) Java, it’s very verbose, doesn’t allow for much “elegance”, and many find it not very fun to write. But it is a pretty decent language for getting shit done.
Overall, I don’t really enjoying writing Go, but it’s not the worst either. I’d code in it if necessary, but wouldn’t chose it for a personal project. I just have more fun and am more productive writing code in concise, mixed OOP/FP languages like TypeScript or Scala, even if they don’t compile as fast.
- Go’s lambdas are extremely verbose. “Concise but clear” is a big part of what people live about functional data structures, Go won’t have the concise part
- Go will still have almost no support for immutability, which works beautifully with functional data structures
Code like this is nice to write:
users.map((user) => user.id)
While code like this isn’t:
users.map(func (user User) string { return user.id })
After such public disregard to the communnity and contributors as a whole, the talk about good or bad has no meaning until they learn the basics. For example how to work with community, and the fact that you have to provide your phone number in order to fix urgent bug or implement some feature is a plain stupid(or rather malicious).
Just imagine you've spent your free time working on the fix or feature, and instead of getting appreciation or sometimes bounty or just nothing, you're being "charged" to contribute. Yeah, they really think it's normal that contributors have to give up PI to the advertisement company that were accused of violating privacy many times before. They basically treat contributors, tech-savvy users who provide free labor, like their usual consumers. Just think about it for a second, this is insane.
one particular thing that tells that is the attitude to interfaces:
while in java (and most languages) interfaces are used to tell which contracts a class implements, in go it’s reversed. you must declare interfaces to _require_ certain contracts, for arguments in your functions
for example:
type interface Operator { Operate(int, int) int }
func IntOparation(a, b int, op Operator) int { return op.Operate(a, b) }
this is a major difference highlighting the ownership boundaries: * when I write a package and rely on a 3rd party contract, instead of referencing it and adhering to it, I will copy-paste parts that I need to my package and be independent
What the fuck?
There is a way to specify what file is actually targeting inside the file, through //go: "pragmas" too
[1] https://stackoverflow.com/questions/25161774/what-are-conven...
Now a bit more background, this goes back to C and C++, and is considered a best practice to name translation units as name_os_arch or similar pattern, with the header file being only name, instead of #ifdef spaghetti.
This is actually one of the few things I think Go did right.
...seriously wtf
It will catch someone out who uses BSD as an acronym for something in their domain model (Bulk Sales Discount?) then xyz_bsd.go doesn't compile.
If a file's name, after stripping the extension and a possible _test suffix, matches any of the following patterns:
*_GOOS
*_GOARCH
\*_GOOS_GOARCH
(example: source_windows_amd64.go) where GOOS and GOARCH represent any known operating system and architecture values respectively, then the file is considered to have an implicit build constraint requiring those terms (in addition to any explicit constraints in the file).I know Go isnt great or perfect, but still, why so much hate? I seriously want to know
But also, I don't think it's specifically hate, it's more of a reaction to the overwhelming wave of posts here (and basically on every programming forum) from around 2014 to 2018.
Go was so hyped it was unavoidable that if you were starting a project, dozens of comments would be shouting at you to use go.
Some posts are people finally getting to say, "I told you so, but I was against the crowd a few years ago" Some posts are people saying, "Go doesn't really fit this use case" Some posts are people just academically sharing the language features you don't get when you choose go.
Basically, in my opinion (and as a developer of a large go codebase that I really love), thousands of people hyped up go as a silver bullet or a "near-perfect" language. This is obviously not true, go has many downsides. When you tried to bring them up before, you were downvoted and pushed aside for the hype. Now that people are maintaining legacy go code, there's more appetite for these conversations about go's tradeoffs.
I especially find it funny that people assume that people like Rob Pike and Ken Thompson who have published research papers on computer science forgot or misunderstand modern language features. They purposely made their language this way, for better or for worse. They never stated that they were trying to make the next Java. And all of the successful projects that have been released so far written in Go is proof enough that it seems they know what they're doing.
People complain online for a language they won't use instead of using the languages that are "superior" to make useful modern software.
Now there's a lot of Go code in the wild, projects people want to use or extend and programmers who will choose it by default, and it's starting to displace C#, Java, and C++ where the comparisons become a lot more preferential and vague. Do you want a faster GC or deterministic allocation? Nominal or structural interfaces? People fear change, especially in a field where change is only loosely correlated with improvement.
Yes, because it is easy and predictable to track exceptions in nested try catches and hidden control flow.
And everyone agreed so hard that it was removed from almost every modern C replacement (Rust, Nim, Zig, Elixir, Kotlin).
val foo = if (bar) "this" else "that"
and you don't need two separate assignments.They removed it because they made the regular "if" statement into an expression, so the separate "ternary" operator expression is superfluous. Rust et al made the "if" situation better than C; Go made it worse.
Rust, Nim, Zig, and Go are all fair game in that list, as far as I know being a C replacement is in the mission statement for the creation of all those languages.
https://ziglang.org/documentation/master/#if
Also, Zig is so into expressions over statements that they plan to remove “function definition as statement”, which I think is awesome. In other words, conceptually function definition is a compile-time expression that creates the function.
Edit: here’s that Zig rfc:
foo = if x, do: 1, else: 2