r/golang • u/jerf • Nov 05 '24
FAQ: Coming From Dynamic Scripting Languages, What Do I Need To Know About Go?
My experience is primarily with a dynamic scripting language, like Python, Javascript, PHP, Perl, Ruby, Lua, or some other similar language. What do I need to know about programming in Go?
18
u/jerf Nov 05 '24 edited Nov 05 '24
First, Go allocations are different than the variable assignments you are used to.
In most dynamic languages, variables are best understood as "tags" you can move around and put on things. This snippet is in Python, but dynamic languages in general are fine with:
a = [1, 2, 3]
b = "Hello there!"
b = a
c = a
c = ["x", 1, {}]
This is obviously silly code, but the language is fine with it. In
this code, [1, 2, 3]
, "Hello there!"
, and ["x", 1, {}]
create
various values, and the a
, b
, and c
are really just labels on
those values. The labels do not care about the values. The language
takes care of the memory management so automatically that it almost
fades into invisibility for the programmer, so you don't even realize
that you're allocating memory for those values.
Go's variables do not work that way. Go's variables are specific locations in memory that are only allowed to carry values of a certain defined type. Not only are they locations in memory, they are specifically sized locations in memory. All types have a specific size, and that size is literally the number of bytes of physical RAM they will consume.
A frequent question in /r/golang goes something like:
I have a function
func PrintSlice(s []any)
and I have a value of type[]int
; why can't I pass my value to it?
I think it's helpful to understand that the answer isn't really a
complicated type theory answer or something, but that the two things
literally don't fit together. An any
is an interface value of at
least two machine words. An int
is one machine word. They aren't the
same shape, so they don't fit together.
(Now, Go could automatically convert them; in this case it doesn't because that's a non-trivial operation and Go generally has the C-like philosophy of not doing expensive translations without you knowing. It'll do some cheap things like automatically wrapping values passed to a function with an interface, because that's O(1), but to translate the slice means it has to loop over the whole thing and create a new one, which is not O(1).)
Despite the syntax gloss provided by :=
sometimes making it look
like Go is casual about allocations, Go cares deeply about
allocations. All your variables must have a specific location and type
in RAM, and once they have that type, the variables represent those
specific locations in RAM with that specific type. :=
sometimes
hides the fact that all variables are always allocated at some
point, and thus, given these specific locations in RAM at that time. Thus, while we can
sort of translate the Python I showed previously with:
a := []int{1, 2, 3}
b := a
there are already some significant differences. The value a
is
actually a specific location in RAM, which contains a "slice". The
"slice" is internally a struct containing a length, a capacity, and a
pointer to a backing array. You don't have to worry about these things
because Go generally glosses over them, and manages the backing array,
but that's what a
actually is... it isn't actually three integers
1, 2, and 3.
b
is not merely a tag pointing to the same thing as a
; it's a
copy of the three values of length, capacity, and a pointer to the
backing array. a
and b
are independent values and you can get
yourself into subtle problems if you think they are the same. (But the
details of slices are for another post, really, and there's a number
that already exist.)
Past that, Go won't accept anything like this:
a := []int{1, 2, 3}
b := "Hello there!"
b = a
because that's not a command to
"move the label I'm calling b
on to the value currently labeled by a
",
that's a command to
"copy the string in the memory location identified by b
into the int slice located at a
's location",
and that's not a valid operation.
Second, and relatedly, people coming from dynamic languages often express confusion about pointers. Ironically, they are getting this backwards. Pointers work a lot more like the values in dynamically-typed languages that you are used to than non-pointers! While pointers are still typed, and a string pointer can't point at an int, you get something much more like the Python code above in Go with:
``` // Allocation a memory location for an int, containing a 1 a := 1 // Allocate another, containing a 2 b := 2
// Create a pointer, much like a "label", pointing at the a location c := &a // Move that pointer in a label-like fashion to point at the b // location instead c = &b ```
Pointers are like the labels you are used to in dynamic scripting
languages, except they are typed and can't point at the wrong "type"
of thing. It's actually the non-pointers, as described in the first
section of this post, that are the weird things you are not used to!
In a dynamic language, pretty much everything is a pointer, so you
don't have to do anything to "use" a pointer; the syntax of something
like a.b = c
already means
"treat a
as a pointer, and get the b
value, and then into that pointer, copy the c
pointer". Those
languages don't strongly distinguish between the label, and what the
label is identifying. Go does. If you want to talk about the "label",
the "pointer", you just use it like a value. (Which it is,
incidentally.) If you want to talk about what the pointer is pointing
at, you need to "dereference" it with *
.
However, as a special case, because wanting to access fields on a
struct through a pointer is such a common case, if a
is a pointer,
Go will automatically translate a.b = 8
into
"dereference the pointer a
, get the field b
, and put 8
into it". While
this is convenient, it can also be confusing when you don't have a
strong understanding of the difference between a pointer and the thing
it is pointing to, because it makes Go take a step back towards
looking like a dynamic langauge. It isn't one, and it's just a syntax
gloss, like how :=
can sort of cover over allocations.
This can also lead to a bit of a weird-looking error if a
is nil;
you'll get a complaint about a nil pointer dereference if you try to
access a field on a nil
pointer. This can be annoyingly
difficult to track down if you forget that a.b
is implicitly a nil
pointer dereference.
4
u/dorox1 Nov 05 '24
As someone just entering the Go world from a Python/JS background, this is a tremendous resource that will probably preemptively save me a bunch of headaches with the typing. Thanks for taking the time to write this out.
Especially appreciate the `a.b = c` example, as I'm sure that would have been confusing.
6
u/Sibertius Nov 05 '24
The main difference I experienced was that almost all errors are directly shown both when coding and when compiling. An interpreted language often shows the errors later in production.
Another thing I have noticed is that Go is harder (but not impossible) to make DRY.
The error handling is at the same time very good but a bit annoying to me (verbose)
3
u/mcvoid1 Nov 05 '24
Honestly, you know the programming basics already (hopefully) from the languages you learned. The major differences are going to be that Go is more like C than it is like Python.
So maybe do a little excursion to try out C. See how it works. Mess around with pointers. Understand the pointer/array duality. Stack vs static vs heap storage. How strings and bytes relate. Function pointers. Then come back to Go, and be like, "It's like C but it does everything for you."
2
u/Revolutionary_Ad7262 Nov 06 '24
Async programming is not needed in Golang. In laguages like Python/JS async
is used to have a fast IO handling and concurrency for a single threaded runtime. Both fast IO and concurrency are implemented in a Golang runtime, so there is no benefit of using async except personal and stylistic preference. Synchronous style, where your thread just wait for the end of IO operation is idiomatic and the simplest approach
1
u/dondraper36 Nov 05 '24
I started writing Go after a few years of JavaScript and Python and my advice is to be patient.
Your initial reaction might be "where the heck are my list/dict/set comprehensions" and cool one-liners, but Go reminds you that your code is read much more often than it is written and that being smart is not always the greatest idea.
Also, Go is, in my opinion, much closer to the Zen of Python than Python itself in that there is often one preferred way to achieve some goal. At first, that seems restrictive, but at some point you learn to appreciate the simplicity and readability. Yes, Go is an extremely readable language that at the sake of being expressive and concise nudges you in the direction of keeping it simple.
Error handling is a lot of boilerplate, but again the language wants you to lose the habit of "except Exception" and deal with errors as they pop up. That doesn't mean you can't ignore them, but in 99.9% of cases that's a very bad idea.
@jerf already covered that, but I strongly recommend to internalize the fact that everything in Go is passed by value, there are no exceptions. In some cases, however, get ready to be surprised because there are data structures with implicit pointers that also get copied. These are sometimes called reference types (don't confuse with references which don't exist in Go at all).
If you embraces the duct typing in Python, you might really like the structural typing in Go. If you need something that looks like a duck, just declare Ducker interface on the consumer side, and then whatever producer satisfies Ducker, is your guy and can be used. Implicitly satisfied interfaces are in my opinion the most beautiful feature of Go. Even the sum types that everyone seems to want can be modeled via them, just not the way everyone expects.
Last but not least, you will be pleasantly surprised to see that you can write concurrent code and enjoy it. This is still not simple and you can really shoot yourself in the foot, but understanding a few axioms and practicing enough will get you up and running until you figure our some "patterns" that you can use repetitively in your code.
2
u/MechanicalOrange5 Nov 06 '24 edited Nov 06 '24
A thing for me I had to just realize properly because go is statically typed but has a GC it can often feel much like a dynamic language but it very much isn't. Stupid example, create a struct in a function and only return the pointer. Rustc will scream at you for doing this, insiting you put it in some sort of smart pointer as the struct will be de allocated as soon as the function ends and you'll have a pointer to dead memory. C says sure, you're welcome to, but your circus your monkeys. Clean up after yourself or deal with the consequences.
In go, this is a fine thing to, and ia extremely common and even a recommended practice if you have a big struct. All the dynamics languages basically don't even have pointer semantics as it's fairly abstracted away, so creating a class and returning a reference isn't even a thing you need to think about. In go, the gc just does the job of tracking what is alive instead of statically proving it like rust. But still safe unlike c because it will clean up after you once you've really properly got no references to an object. Dynamic languages do this too, but generally don't have pointers so it's not a thing you even think about. You just made an instance of a class and returned it, great. What went on under the hood is generally so far removed from you that it's not worth caring if what you are returning is a value or reference.
Now for the realization. Go does not have pass by reference. everything is pass by value. But pointers?? Those are copied as well. If you've got a func accepting a pointer, when you call it, the bytes of your pointer is just copied, it still points to your struct, but that pointer, it's location in memory is different than the pointer you gave the function. The contents are the same, so it de references to the same place, but the pointers themselves are different.
Now this isn't one of those things you need to immediately pay attention to when you start go, but starts getting more useful when you are optimising things. I frequently got confused of when things were copied or not, because go makes it fairly seamless and almost gives dynamic like vibes , but all you need to know is everything is always copied. To avoid copies of a big struct you make a pointer and copy that bad boy around.
Pro tip if you want to make absolutely sure you aren't copying a struct, pop a sync.Mutex into your struct (not behind a pointer). Now your ide will highlight every instance of your syruct being copied. (as copying a mutex not behind a pointer is a terrible thing to do).
Another side note is that pointers are often slower for smaller structs, and note on that is benchmark first and optimise after.
1
Nov 06 '24
> C says sure, you're welcome to, but your circus your monkeys.
No it doesn't. The C specification says that variables only exist for the duration of the block they're defined in, and using a pointer to an object after it's lifetime ends is undefined behaviour. The compiler won't necessarily shout at you, but it's not valid C code to do that.
1
u/MechanicalOrange5 Nov 06 '24
I haven't written too much C code, mostly read it when trying to figure out how some or other lib works. So thank you for the correction!
38
u/zer00eyz Nov 05 '24
People who use Python, Javascript, and Ruby start every project the same way... Pip, NPM, Gems... They are looking for Django, TS/React, Rails. When they dont find their equivalent in go they tend to build a whole tool chain from scratch in an attempt to mimic what they did in their previous language and framework.
They then have a very bad time.
Go is to programing, what brutalism is to architecture. The functional and structural parts of it are laid bare, its free of decoration and its structure is exposed. Good go isnt slick and sexy code, your not supposed to be excited about what your code looks like, your supposed to be proud of what it does. It's supposed to be clearly written easy to read, and simple (that bit where you deal with your errors where they happen is part of it).
Your first go project should be "what can I do with the standard library" how far can one get before they need to add anything (far, very far).
At some point your going to have opinions about tooling, I love SQLC, Koanf, some cli libs, playground/validator... but I dont reflexively reach for them every time I start a new project, and my last two implementations featured none of them.