Learn Go: Hand-crafted Go exercises and examples(github.com) |
Learn Go: Hand-crafted Go exercises and examples(github.com) |
I often wonder if one of the reasons Go is more "simple" or approachable to some is because you can, to a large extent, ignore pointers and interfaces and "just write that weird little * or & in some places" and get away with it. Whereas, I believe in other languages, this is much less possible (e.g. in Java or Rust you need to learn about less "just logic" traits of the language earlier on (class inheritance, generics or Options, borrow checking, etc.)
I'm not saying the above is a good thing, but I often wonder if there aren't ways to make this more of an incremental learning curve in other languages in a similar way that you can largely ignore pointers in Go for a long time and be productive without understanding them. What would a similar incremental learning curve for generics be?
Slices are hard to grasp, you can modify them within a function (without ever touching that weird little * and his & cousin) and they are still modified when you exit the function, but you can't do that with other values (all of them need the star operator if you want to modify them).
Plus, appending a value to the end of a slice is very cumbersome and unusual with something like
arr = append(arr, val)
Oh, and do you remember when i said you could modify slices within functions and they are still modified in the end? This is not true with append. If you append to a slice, the new values won't be appended when you return from the function.This thing doesn't really make sense unless you've been a C programmer in fact, and understand the concept of fixed-size arrays and reallocating memory to grow them. Otherwise, even if you can work around that weird part, it will always seem a little magical and tricky.
type Foo struct { ... }
func (f *Foo) Bar() { ... }
And go "Aha, that's an object and a method, I get it!" but when asked whether they should be using a pointer there or not, have no idea what that even entails.In other words, it's possible to neglect the details of pointers easily and have things generally work
ᕕ(¬ ͜ ¬)ᕗ
#!/bin/bash
cd $HOME
# Create project directory
mkdir hellogo
cd hellogo
# Init project modfile
go mod init example.com/hellogo
# Create the main source file
# Uses bash's heredoc
cat << EOF > ./main.go
package main
import "fmt"
func main() {
fmt.Print("Hello world")
}
EOF
# Build it
go build
# Run it
./hellogoThen, go get -v will download your dependencies to GOPATH, and (re)generate go.mod, go.sum. For info on how to update dependencies, read `go help get`.
When using modules (presence of a valid go.mod file), GOPATH won't be used for resolving imports. So you don't have to actually work in GOPATH anymore, dependencies are just stored there.
It will then run way faster and be completely bug free.
Go's main advantage, IMO, is that is both simple and opinionated to the extreme. It's a language that makes a lot of choices for you (from "no while loop" through "no exceptions" to where the brackets go formatting the file). Those choices might be the best choice or not, but the point is that they're already made and they're enforced.
The result is that in a big company, all projects made in go are very similar to each other in a lot of ways (much more than the average language) and so it is as easy as possible to have people contributing in each other's projects or moving between teams. There's no arcane syntax, no discussion about linters, nothing like that.
It's basically an amazing language to make programmers interchangeable. Whether that's good or not is a different discussion, but at the very least it looks that it's gonna make it very attractive for employers and so be a highly demanded skill in the near future.
My main quip with Go is that its rather more of the same but more poorly done in the name of minimalism. Extreme minimalism is as bad as extreme richness. The entire art of language design is achieving just the right balance for a specific audience and set of goals.
It purposefully leaves out lots of features and cutting edge design philosophies because many Of those make things difficult when sharing code between multiple developers Spread over a long length of time.
Go’s philosophy has always been close to KISS. Don’t provide all these cutting edge tricks and tips because you can accomplish the same thing in a far clearer and maintainable way by doing it simpler.
They certainly got some things wrong (null for example, and errors aren't great, generic collections would be nice), but it's quite interesting just how much they left out while making a very usable (IMO) language.
I don't want advances and progress in a programming language, I want it to get out of the way and let me think.
I mean I see your point, and if I really wanted to be fully functional I'd use a language _with_ those things like Haskell. But not being mutable is easy in Go too.
I'd call them more like working standards. Imagine having to discuss how to make a brick wall everytime you need to make one. Things like having a masonry cord (which can equal to a "gutter", line length restriction) and the mixture of the cement, are critical for execution in a team of more than 1 person, and not following standards will make a hacked up job.
But you can't do that with open-source libraries. When I use an open source library, I can audit its code very easily, and this is not always true with other languages. The more complex the language, the harder it is to audit it. There are many C++ open source libraries (hello Boost) I can't understand because they don't use the dialects I'm used to.
Loop variables are captured by "reference", not by value, so it's very easy to create bugs where you capture accidentally capture the wrong thing and don't have the value you'd expect.
Nulls, the billion dollar mistake. Most languages are quickly moving away from nulls (and pointers for that matter), or creating constructs that make them much safer (such as what typescript is doing). Instead go doubles down on both of these. In the last decade of programming in kernel level c, python, ruby, c++, java, typescript, scala I've never worked with code that crashes so much and is as buggy as go.
Both of these problems could have been addressed fairly easily without bloating the language. Google has people on the c++ committee; it very much feels like the creators of go had too much hubris to walk down the hall or across the campus and kindly ask a good language designer to shoot holes in their design.
Another example is "iota". How does that make code sharing easy at scale? Any time I see iota, I have to start manually counting lines, and remember the rules for if blank lines or comments bump the counter, and it completely circumvents the ability to quickly grep for a value seen in a log message. It is completely antithetical to teams of people and spans of time and whatnot. It seems more like a team of three people who randomly had ideas and ran with them without thinking the consequences through very well, or consulting the wisdom of others.
That "billion dollar mistakes" is an excellent marketing expression (nobody wants to make billion-dollar mistakes! We should avoid that null they talk about! It looks so expensive!) but I don't know how actually true it is. Do we have any kind of scientific paper that proves languages without null lead to way less expensive software than languages with null?
I'm not talking about memory unsafe languages like C or C++, but situations where, in a memory-safe language, a null pointer exception in production happened to cost a shitload of money.
I'm pretty sure I never had a nil dereferencing in production with my go code. Invalid array access, off-by-one-errors, yeah, sure, way too many, but very few languages can prevent them at compile time. But nil dereferencing? I can't remember that.
Programming in Go is like trying to tie your shoes with one hand. You can do it, and its okay (I guess), but you can always see how things could be so much easier.
Maybe this is a bit much, but I find Go offensive: designed for the maximum amount of typing and least amount of thinking. Even Java looks like a powerful language in comparison.
that comes out to 20 loc per 8.5x11 piece of printer paper.
I feel like that's nearly an order or magnitude too low.
So, pass small things ByVal (int, float), it's on the Stack.
Pass big things (Object) ByRef cause they're in the Heap.
Then I start saying pointer more than ByRef and the link is made.
Then on to ByRef/Pointer to how that then manipulate the shared data.
Once that basic is done, we refine/clarify around what Pointer really is, and also it's syntax.
[1]: https://doc.rust-lang.org/stable/book/ch05-03-method-syntax....
How was your learning experience with Rust's borrow checking and pointers? Was it introduced to you early on, or later on?
I like when things are explained upfront, and actually my biggest issue when learning Rust came from not having the smart pointers (like RefCell and Rc, which relax the ownership constraints) explained at the same times as regular ones.
func (c Coworker) SendForUpdates(d Document) {
...
}
That wouldn't make sense. You worked hard and I don't even know what you did. So, what I would expect you to do is, once you made updates on the copy, to send me back that copy by email. That would be akin to func (c Coworker) SendForUpdates(d Document) Document {
...
return d
}
I sent you a copy, and you returned another updated copy. That is "pass-by-value", the default, no-pointer style.Now, let's say I think those emails back and forth and boring. Rather than sending you a copy of the text each time, I could rather use Google Docs, and send you the link to that document. Its URL, rather than a copy of its content. Now, you can just go to that URL and do the updates on the document. You don't have to send me back the document: you're working on it, not on a copy of it! Well, that URL is a reference to the document rather than the document itself, or, if you prefer, a pointer to it. So, now, the function would be
func (c Coworker) SendForUpdates(d *Document) {
...
}
And we're done, no more back-and-forth dance now! That is "pass-by-reference".You don't only use "pass-by-reference" just to be able to check updates on the document sent, by the way. If I want to send you some text just for your information and I don't expect any kind of update, I'll use pass-by-value (the very first function). But what if I want to send you a 3 GB video? I can't send that through e-mail! Sending a copy would be totally inefficient. Once again, I'll send you a pointer, an URL to download the video:
func (c Coworker) InformText(d Document) // d is small: pass-by-value
func (c Coworker) InformBigVideo(v *Video) // videos are huge: pass-by-reference
Why not use pointers everywhere by default, they seem easier, right? That's basically what java and python do. Well, they can be tricky too. I gave you the URL to the link and you could work on it. Once you're done, I don't want you to modify the document anymore. I want to send it to our boss. But, how could I know you didn't keep the URL somewhere in your bookmarks? How do I know, of all the coworkers I sent the URL to, one of them doesn't keep on updating that document even when I don't want to anymore? With copies, I'm safe, do whatever the hell you want with your copy, I don't care anymore. But a reference to the original document? That can be dangerous.But,you can easily make the classes/collections immutable to avoid the issues you mentioned. I think in java records and many collections are immutable by default. Immutability is fundamental to functional style programming.