The most important thing to know is that Go slices are exactly how you would implement a dynamically growable slice in C if I asked you to. No less, but importantly, no more: a pointer to a piece of memory, and a current length and capacity.
The classic slice mistake is to forget that append doesn't necessarily return a completely new slice backed by a disjoint piece of memory, especially easy if you've been using a language where variables are immutable by default.
The problem is that it will often accidentally work; if you over-run the backing store, you will get a new piece of memory:
a := make([]int, 1, 1) // a is backed by a one-element-long piece of memory
a[0] = 42
b := append(a, 17) // this must allocate a new piece of memory
c := append(a, 37) // this must also allocate a new piece of memory
// At this point a == [42], b == [42, 17], c == [42, 37]
The problem is when the existing capacity is sufficient:
a := make([]int, 1, 10) // a is backed by a ten-element-long piece of memory
a[0] = 42
b := append(a, 17) // no need to allocate: just use the existing memory
c := append(a, 37) // no need to allocate: just (re)use the existing memory
// At this point a == [42], b == [42, 37], c == [42, 37]
// ^^^^^^^^^^^^^
ustad 38 days ago [-]
Go's slice behavior reveals a concerning trade-off in language design where performance was prioritized over predictability. Consider this trap:
a := make([]int, 1, 10)
a[0] = 42
b := append(a, 17)
c := append(a, 37)
// Surprise! b and c both equal [42, 37]
The fact that append()'s behavior changes based on a hidden property (capacity) violates what I consider a core principle of language design: fundamental operations should be predictable and easy to reason about without having to think about implementation details.
While I understand the performance benefits of reusing memory, language fundamentals shouldn't require developers to constantly think about backing array capacity to avoid bugs. This is especially problematic because it "usually works" until it doesn't - the worst kind of bug.
Modern languages have shown us better ways. Rust makes ownership explicit. Immutable-by-default languages prevent such surprises entirely. Even if Go wanted to maintain performance, they could have made this behavior more explicit - perhaps with separate "append_safe" and "append_reuse" functions.
Programming language fundamentals should be like good UI design - they should do what you expect them to do. When core operations have surprising edge cases, it creates cognitive overhead that ripples through all code written in that language.
never_inline 37 days ago [-]
Usually the idiom is using the same variable on LHS.
```
a = append(a, elem)
```
If you assign it to anything else, `a` might still remain with the older slice memory which will cause worse problems than equality comparison.
So the kind of code you wrote is hardly ever written in production.
The advantage of having this is avoiding one more heap allocation for slice, which is a good tradeoff if you ask me.
deergomoo 37 days ago [-]
In production almost every
`foo, err := callSomeFn()`
is followed by
```
if err != nil {
return err
}
```
too; it’s the ones that get missed that are the problem because convention will do nothing to prevent silly mistakes.
Like many things in Go, there are steps the language could take but chooses not to. For example, Swift implements copy-on-write for variable-length arrays. I’m not qualified to comment on whether the Go team is right or wrong in their decisions, but these aren’t unsolved problems.
ternaryoperator 38 days ago [-]
Honestly, I couldn't believe that this code works as you stated it does. But I ran it in the Playground[17] and sure enough! That is exactly what it produces. Ugh!
oh i never thought about y = append(x, ...) in which scenario would make sense to have different x and y?
neillyons 38 days ago [-]
wow.
kccqzy 38 days ago [-]
I'd argue that the Go example is worse than the equivalent in C, because Go has a garbage collector and C does not. In C, precisely because of the lack of a garbage collector, people need to explicitly and carefully document who is responsible for deallocating the memory. This makes things clear who is owning that memory. And this by extension leads to an intuition on whether the append is acceptable on the original slice or not. If a C function returns such a slice and is documented that the caller should free it, you can call realloc and add to it; if the C function documents that the caller should not free it, you naturally wouldn't even try to append to it: you would copy it first.
In Go, this garbage collector frees people from thinking about freeing memory. But it doesn't free people from thinking about owned vs borrowed memory. And that's where bugs can happen.
zellyn 38 days ago [-]
I happen to like the way Go did it, but now that generics and iterators have landed, it should be possible to create an always-make-a-copy vector type (although it'll need to use `get` and `set` like Java Lists, not [42] like slices/arrays).
It's not like you could have appended twice to the same List in Java and expected to get two disjoint arrays, unless you copy first, and `slices.Clone()` makes that easy in Go now too.
kbolino 38 days ago [-]
This isn't really a practical problem.
If you created the slice, you control it and you can modify its elements, append to it, etc.
If you didn't create the slice, you don't control it. If you want to modify it or append to it, you should copy it first.
This has reflected how I've seen Go used professionally by people experienced in the language. The language could offer more help (but probably never will), but it's not hardly a regression from C. The real regression from C is the lack of const pointers.
pjmlp 38 days ago [-]
And enums,
type Colours (Red, Blue, Green)
//....
c := Colours.Blue
a := pred(c)
b := succ(c)
for x := range Colours {
//....
}
1976's Pascal style is unthinkable to ever land in Go, but we have iota and const, lets not complain.
kbolino 38 days ago [-]
Go's "enums" are worse than a lot of other languages, but they're not worse than C.
pjmlp 38 days ago [-]
Indeed, then again maybe we should not design languages with features worse than C in the 21st century, having 60 years of field experience.
kbolino 38 days ago [-]
It's not worse. It's just only slightly better. The most damning thing you can say about Go is not that it doesn't improve upon C, it's that it improves only on C (and only in the ways cared about). The authors of Go really didn't examine other languages very closely. So starting with the authors' goals (light on the page, bounds checked, concurrency with message passing, easy to pick up, fast to compile) and a fairly deep knowledge of C (but little else) you pretty much get Go (especially early Go).
pjmlp 38 days ago [-]
Even that isn't really the case, as Go, as it started, was basically Inferno's Limbo in a new clothing, with some influence from Oberon-2 method syntax and the SYSTEM package as unsafe.
Unfortunately afterwards they decided to follow Wirth's quest in Oberon-07 minimalism, instead of Active Oberon.
baranul 35 days ago [-]
I've always felt that the behavior of Go's ownership, in regards to minimalism, is a reflection of it being considered a solution for their internal problems. This is something outside of what users are requesting or needing.
People have to look at Go alternative languages like Goplus, V (Vlang), Borogo, etc... for wanted features or greater responsiveness to user demands.
9rx 38 days ago [-]
Go enums are the same as in every single other language. After all, all an enum does is count. There isn't anything more you can do with it.
You can introduce data structures and types that utilize enums, with some languages taking that idea further than others, but that's well beyond enums themselves.
masklinn 38 days ago [-]
> Go enums are the same as in every single other language. After all, all an enum does is count. There isn't anything more you can do with it.
You may want to meet a modern language one day. Or hell not even a modern language, even a shit one like Java.
As it turns out there are languages where you can define an
enum foo { bar, baz }
and when you type a parameter as a `foo` you're not at risk of getting 69 or 4328.
noapologies 38 days ago [-]
> ... even a shit one like Java.
I agree with your point about Go enums.
But in defense of Java, modern Java is actually pretty pleasant.
Virtual threads, records and sealed classes, pattern matching, state-of-the-art garbage collectors, a great standard library etc. (and obviously well-behaved enums).
Not to mention the other languages you get for free with the JVM ecosystem.
It might not be as expressive as Rust, but certainly Java/JVM > Go.
pjmlp 38 days ago [-]
It is so funny how Go folks praise its standard library, when it is a fraction of Python, Java and .NET standard libraries.
kbolino 38 days ago [-]
Things that were in the Go standard library from the first public release (2012):
encoding/json vs
Python: 2008, json package added in version 2.6
.NET: 2019, System.Text.Json came with .NET Core 3.0
JVM: still nothing
net/http client vs
Python: gave up, "just use requests"
.NET: also 2012, HttpClient came with .NET Framework 4.5
JVM: 2018, HttpClient came with Java 11
The time library definitely sucks though. There's really no excuse for it, joda-time came out a long time ago.
pjmlp 38 days ago [-]
Cherry picking examples are we?
What about actually counting the set of features across a full stack application?
I can pick countless of examples of features in Java and .NET standard library that Go 1.24 still doesn't have any answer for.
kbolino 35 days ago [-]
No, I don't think HTTP or JSON are cherry-picked. Depending on what kind of code you write, maybe you haven't needed them as often as I have.
Of course, plenty of high-quality third-party libraries exist in the other languages. I've used commons-httpclient and jackson in Java, and everybody and their brother knows about requests in Python (though I prefer aiohttp nowadays). But they are odd omissions from the standard libraries of those languages. At least .NET caught up.
37 days ago [-]
38 days ago [-]
38 days ago [-]
38 days ago [-]
9rx 38 days ago [-]
> You may want to meet a modern language one day.
Like what? CrabLang? Its enums are identical to Go, unsurprisingly. Above the enum rests a discriminated union that ends up hiding the details of the enum. That is where things begin to differ greatly. You have to do some real trickery to actually get at the underlying enum result in that language. But, when you do, you see that it is exactly the same.
> and when you type a parameter as a `foo` you're not at risk of getting 69 or 4328.
That's thanks to the type system, though, not enums. Enums are not a type. Enums are a number generator. Hence the name.
tialaramex 38 days ago [-]
> That's thanks to the type system, though, not enums. Enums are not a type. Enums are a number generator. Hence the name.
What's happened here is that you've mistaken "Things I believe" for "What everybody else believes" but you're talking to other people, not yourself, so, this makes you seem like a blithering idiot.
The particular version of this trap you've fallen into is a variation of the "It's all just the machine integers" mental model from C. It's just a model and the problem arises when you mistake that model for reality.
Now, technically this model isn't even correct for C's abstract machine, but it's close enough for many programmers and it tends to match how they think the hardware works, which is even more confusing for the hardware people who know how it actually works but that's another conversation.
This model is completely useless for languages which don't have the same type system, and so it's no surprise that it immediately led you astray.
9rx 38 days ago [-]
> but you're talking to other people
No, I am clearly talking to a computer program. It is possible that the program is forwarding the discussion on to people. Perhaps that is what you are trying to allude to? The details of how the software works behind the scenes is beyond my concern. There is no intention to talk to other people, even if the software has somehow created that situation incidentally. If I wanted to talk to people, I would go out and talk to people, not type away at my computer into a box given to me by the software.
> The particular version of this trap you've fallen into is a variation of the "It's all just the machine integers" mental model from C.
As much as I enjoy your pet definition that you've arbitrarily made up on the spot here, the particular trap I have fallen into is the dictionary. It literally states what an enumeration is according to the prevailing usage. It does not describe it as a type, it describes it as the action of mentioning a number of things one by one. Which is exactly what an enum does.
The previous comment is talking about type constraints. You can definitely constrain a type such that it is invalid to use it outside of the numbers generated by the enum, just as you can constrain a type to only accept certain strings. e.g. from Typescript: `type Email = "{string}@{string}"` This idea is not limited to enums.
That's definitely a thing, and definitely a thing that could be used in conjunction with an enum, but is not an enum itself. Not as enum is normally used. Of course you can arbitrarily define it however you please. But, if you are accepting of each holding their own pet definition, your comment doesn't work. You can't have it both ways.
pjmlp 38 days ago [-]
Go enums are stuck in the 1960's macro assemblers.
9rx 38 days ago [-]
Quite right. The entire possible space for enums to explore was exhausted in the age of 1960s assembler. There is only so much you can do with a number generator.
Which, I guess, is why we have this desperate attempt to redefine what an enum is, having become bored with a term that has no invitation potential. But, unfortunately, we have not found shared consensus on what "enum" should become. Some think it should be a constraint, others think it should be a discriminated union, others think a type declaration, so on and so forth.
All of which already have names, which makes the whole thing particularly bizarre. You'd think if someone really feels the need to make their mark on the world by coining "enum" under new usage in the popular lexicon they would at least pick a concept that isn't already otherwise named.
pjmlp 38 days ago [-]
I think some people need to spend some time learning type systems theory.
9rx 38 days ago [-]
That is probably true, but has little to do with enums, which are concerned with mentioning things one-by-one (i.e. producing values).
It is true that some type system features built upon enums. Like a previous commenter mentioned, Pascal offers a type that constrains allowable values of that type to be within the values generated by the enumerator. Likewise, I mentioned in another discussion that in CrabLang the enumerator value is used as the discriminant in its discriminated union types, which achieves a similar effect. I expect that confuses some people into thinking types and enums are the same thing, which may be what you are trying to get at, although doesn't really apply here. The difference is known to those reading this discussion.
The biggest problem with this desperate attempt to find new meaning for "enum" is: What are we going to call what has traditionally been known as an enum? It does not seem to have another word to describe it.
kccqzy 38 days ago [-]
The practical problem is about transferring the control. Without it, you end up doing way too much defensive copying.
BoingBoomTschak 38 days ago [-]
That's not the "classic slice mistake", that's slices being one of the stupidest things Go ever decided upon. In C++, that'd be the equivalent of merging std::vector<T> and std::span<T, std::dynamic_extent> into a single structure for "reasons" ("simplicity"?).
In the period when Go was ready for production use and Rust wasn't, I couldn't wait for Rust to be ready, so that the response to everyone hating Go for not being a C++ equivalent could just be, "I think you want Rust."
I think you want Rust.
wavemode 38 days ago [-]
It's not clear to me why I would need a "dynamically growable slice". That's not a slice at that point, it's an vector aka ArrayList.
If all I need is a slice of memory, I'd rather only have to pass around the two pieces of data (pointer and length) than all three.
tialaramex 38 days ago [-]
I think of this as a failed experiment. Like coal powered cars or when we thought maybe transatlantic travel by airship was the Right Thing. It may be hard to know what'll happen without trying.
The growable array type (what you call "vector") is a venerable data type, although it does still have parameters you might reasonable tweak e.g. what's the correct growth factor? Doubling is popular but e.g. Zig chooses new_size = (old_size * 1.5) + 8 and you'll see disagreement about what API should be offered and why - e.g. C++ std::vector has the wrong reservation API
But this thing clearly isn't a mistake, it's intentionally a hybrid, and if it had been a huge success maybe everybody else would scramble to copy it.
slashdev 38 days ago [-]
Indeed, this is how rust handles it.
A mutable String or Vec has all three fields, but the borrowed slice or str has just pointer and length.
Go chooses to be simpler by not having those kinds of separations, which also makes it a little less efficient under some circumstances.
The growable array type is an owning container, whereas a slice (in C++ and C# span) is a reference type, it doesn't actually own anything.
This type isn't either of those things, it's a hybrid, and Go tries to get away with that because it's a garbage collected language so people don't need to care as much about ownership.
jas39 37 days ago [-]
Very disturbing how append sometimes decouples, sometimes not.
eu 39 days ago [-]
it’s a good read, but i think it should focus more on some of the common mistakes that people make when slicing a slice.
rednafi 39 days ago [-]
Slicing a slice is full of gotchas. I tend to forget all the rules and avoid it whenever I can.
adonovan 38 days ago [-]
A slice operation s[i:] seems like it should be little more than an ADD instruction for a registerized slice where i is known to be in bounds, but a surprising little detail is that when i==cap(s) we really don't want to create an empty slice whose data pointer points one past the end of the original array, as this could keep some arbitrary object live. So the compiler generates a special branch-free sequence (NEG;SUB;ASR;AND) to compute the correct pointer increment, ((i - cap) >> 63) & (8 * i).
Appending a slice is also full of gotchas. Sometimes it modifies the slice in place, sometimes it reallocates and copies.
pstuart 38 days ago [-]
Only really a gotcha if you pass a slice into a function and expect to see modifications in that slice after the function completes. It's helpful to remember that Go passes by value, not reference.
> Only really a gotcha if you pass a slice into a function and expect to see modifications in that slice after the function completes. It's helpful to remember that Go passes by value, not reference.
Slices are passed partly by value (the length), partly by reference (the data).
func takeSlice(s []int) {
slices.Sort(s)
}
From your explanation, you would expect that to not mutate the slice passed in, but it does.
This can have other quite confusing gotchas, like:
Slices are passed only by value. It's just that the value is a struct containing a reference to the data. Once one understands that, the rest makes perfect sense.
I can see why it trips up newcomers, but it feels pretty basic otherwise.
lsaferite 37 days ago [-]
I say this as someone working with go every day.
The fact that I can pass a slice to a func 'by value' and mutate the source slice outside the func is already surprising behavior to most people. The fact that it MIGHT mutate the source slice depending on the slice capacity is the part that really drives it home as bad ergonomics for me.
Overall I enjoy working with go, but there are a few aspects that drive me up the wall, this is one of them.
silisili 37 days ago [-]
How would you have designed it? An internal byte array instead of a pointer?
TheDong 37 days ago [-]
I think the key thing missing from go slices is ownership information, especially around sub-slices.
Make it so you can create copy-on-write slices of a larger slice, and a huge number of bugs go away.
Or do what rust did, except at runtime, and keep track of ownership
s := []int{1, 2, 3}
s[0] = 0 // fine, s owns data
s1 := s[0:2] // ownership transferred to s1, s is now read-only
s1[0] = 1 // fine, s1 owns data
s[0] = 1 // panic or compiler error, s1 owns data, not s
With of course functions to allow multiple mutable ownership in cases where that's needed, but it shouldn't be the default
pstuart 37 days ago [-]
I could have worded it better, but yes, slices have footgun potential but it's simple to work with once you know how they work (and maps fall into this same category).
bborud 39 days ago [-]
It is a suprisingly hard thing to implement well. I have no idea how many times I have implemented slice-like things in C (back in the 1990-2000s when I mostly wrote C) and it was one of those things I never managed to be happy with.
pjmlp 38 days ago [-]
Something that even Mesa and Modula-2 already supported by 1980's, maybe one day C will eventually get proper slices.
baranul 35 days ago [-]
Good point. As for C and slices, doubt that many care, at this point. Many will use C alternatives that have slices or are long time C users that just deal with it.
neonsunset 39 days ago [-]
It can be done but that requires a better, more expressive type system.
rednafi 39 days ago [-]
An expressive type system also often means slower build times. I dislike working with Rust for this exact reason.
While most people highlight the difficulty of picking up the syntax, I find Rust to be an incredibly tedious language overall. Zig has a less expressive type system, but it compiles much faster (though not as fast as Go). I like what Zig and Odin folks are doing over there.
I like the balance Go strikes between developer productivity and power, though I dearly miss union types in Go.
4ad 39 days ago [-]
An expressive type system absolutely, positively, unequivocally does not imply slower build times (especially with a Church-style type system). There are plenty of programming languages with advanced type systems which compile extremely quickly, even faster than Go, for example OCaml.
Don't make the fallacy of conflating Rust's slow compile time with its "advanced" (not really, it's 80's tech) type system. Rust compilation is slow for unrelated reasons.
taurknaut 38 days ago [-]
Old doesn't mean non-advanced. GraalVM is based on a paper (Futamura) from fifty years ago. Off the top of my head I can't think of many language features younger than the eighties—maybe green threading? That would be surprising but might fit. I suppose you could also say gradual typing. Haskell has many recent innovations, of course, but very few of those have seen much use elsewhere. Scala has its implicits, I guess, that's another one.
Personally, I write java at my day job and the type system there makes me loooong for rust.
pjmlp 38 days ago [-]
No need for Rust, when JVM has Haskell, Scala, Kotlin, Clojure, Common Lisp.
taurknaut 38 days ago [-]
I prefer rust to all of them, but I also come from a very systemsy background. Plus it has the benefit of being much easier to embed inside or compose around basically any runtime you'd like than managed code, which is why I chose rust rather than basically any managed language.
But, it's just a tool, and the tools I choose reflect the type of stuff I want to build. The JVM is extremely impressive in its own right. You're just not going to to find any one runtime or ecosystem that hits every niche. I'm happy to leave the language favoritism to the junior devs—for the vast majority of situations, what you're building dictates which language makes the most sense, not vice versa.
neonsunset 39 days ago [-]
As a start, Go could separate container and slice types, the way C# did it with T[]/List<T>/other and Span<T>/Memory<T>. No lengthy build process required.
jerf 38 days ago [-]
I'm not deeply familiar with those C# types, but I think maybe it already does. An array, which includes the size of itself in its type so that a four-element array is not the same type as an eight-element array, is already in Go. Go's language affordances make it easy to just have slices without the underlying arrays, since they're generally not useful on their own, but you can take arrays and slice into them if you like.
38 days ago [-]
rednafi 38 days ago [-]
Yeah, but the at the same time, I find C# code a sigil soup. Go makes a different tradeoff.
I've been involved in a few successful large scale projects and never felt like the type system of Go is holding me back too much. Sure the error handling could be better, union type would make interface munging easier, but the current state after generics isn't too bad.
neonsunset 38 days ago [-]
> Sigil soup
Last time I checked, C# had clean and focused syntax for working with collection types. Could you provide an example?
thegeekpirate 38 days ago [-]
You'd most likely be happy with Odin (https://odin-lang.org), which I find to be essentially a fixed Go with no GC.
dist1ll 38 days ago [-]
It's possible to build languages that compile faster than Go, with a much more expressive type system.
It's just that compile times and DevEx haven't been a priority for most projects.
pjmlp 38 days ago [-]
As proven by other languages with similar type systems and faster compile times, Rust's case is a matter of tooling, not language features.
_flux 38 days ago [-]
Not sure if that's really a proof, as it could be the exact combination of language features that makes up the slowness. For example traits, non-ordered definitions in compilation units and monomorphization probably don't help. GHC also isn't a speed demon for compilation.
But sure, LLVM and interfacing with it is quite possibly a big contributor to it.
pjmlp 38 days ago [-]
Haskell isn't the only language around with complex type systems.
However, it is actually a good example regarding tooling, as the Haskell ecosystem has interpreters and REPL environments available, for quick development and prototyping, something that is yet to be common among Rustaceans.
Indeed, but its compile times aren't much better than LLVM, at least one year ago.
Ideally we would be having the F# REPL/JIT, plus Native AOT for deployment, as comparable development workflow experience.
Naturally F# was chosen as example, because that's your area. :)
Not being negative per se, I also would like to have something like Haskell GHCi, or OCaml bytecode compiler, as options on rustup, so naturally something like this might eventually come.
burnished 38 days ago [-]
Based on a first hand account I read (but cannot source offhand) Rust's slow compiles are because anytime there was a tradeoff involving compile time at the expense of something else they'd always choose that something else. Not cause they hated fast compilation, guess it just wasn't high on their priorities.
fmbb 38 days ago [-]
Can you link some article about these common mistakes? Sounds like good learnings can be had.
WolfCop 39 days ago [-]
(2009)
rednafi 39 days ago [-]
It's Go we're talking about. Other than 64-bit the dominant word size, nothing much has changed.
4ad 39 days ago [-]
The interface layout has changed since the article (although this specific article doesn't mention interfaces, a later article in the series does). Additionally Go now has generics.
It's true that little has changed, but very little is changing in the data representation of any language, really. Even ones that are evolving rapidly.
Cthulhu_ 38 days ago [-]
This is a truth that's easily overlooked; most languages are several levels beyond basic types to the point that people forget about the low level constructs involved. This is one reason why I like Go, it exposes and educates on fairly low-level mechanisms that are not unfamiliar to anyone who's studied computer science, but at the same time you don't have to worry too much about the lower level stuff like memory, pointers, zeroing, etc. I think it's a good tradeoff.
mseepgood 38 days ago [-]
[flagged]
Cthulhu_ 38 days ago [-]
HN is not primarily a news site though, despite the name.
The classic slice mistake is to forget that append doesn't necessarily return a completely new slice backed by a disjoint piece of memory, especially easy if you've been using a language where variables are immutable by default.
The problem is that it will often accidentally work; if you over-run the backing store, you will get a new piece of memory:
The problem is when the existing capacity is sufficient:While I understand the performance benefits of reusing memory, language fundamentals shouldn't require developers to constantly think about backing array capacity to avoid bugs. This is especially problematic because it "usually works" until it doesn't - the worst kind of bug.
Modern languages have shown us better ways. Rust makes ownership explicit. Immutable-by-default languages prevent such surprises entirely. Even if Go wanted to maintain performance, they could have made this behavior more explicit - perhaps with separate "append_safe" and "append_reuse" functions.
Programming language fundamentals should be like good UI design - they should do what you expect them to do. When core operations have surprising edge cases, it creates cognitive overhead that ripples through all code written in that language.
``` a = append(a, elem) ```
If you assign it to anything else, `a` might still remain with the older slice memory which will cause worse problems than equality comparison.
So the kind of code you wrote is hardly ever written in production.
The advantage of having this is avoiding one more heap allocation for slice, which is a good tradeoff if you ask me.
`foo, err := callSomeFn()`
is followed by
``` if err != nil { return err } ```
too; it’s the ones that get missed that are the problem because convention will do nothing to prevent silly mistakes.
Like many things in Go, there are steps the language could take but chooses not to. For example, Swift implements copy-on-write for variable-length arrays. I’m not qualified to comment on whether the Go team is right or wrong in their decisions, but these aren’t unsolved problems.
[37] https://go.dev/play/p/rO7rlvK7Dna
In Go, this garbage collector frees people from thinking about freeing memory. But it doesn't free people from thinking about owned vs borrowed memory. And that's where bugs can happen.
It's not like you could have appended twice to the same List in Java and expected to get two disjoint arrays, unless you copy first, and `slices.Clone()` makes that easy in Go now too.
If you created the slice, you control it and you can modify its elements, append to it, etc.
If you didn't create the slice, you don't control it. If you want to modify it or append to it, you should copy it first.
This has reflected how I've seen Go used professionally by people experienced in the language. The language could offer more help (but probably never will), but it's not hardly a regression from C. The real regression from C is the lack of const pointers.
Unfortunately afterwards they decided to follow Wirth's quest in Oberon-07 minimalism, instead of Active Oberon.
People have to look at Go alternative languages like Goplus, V (Vlang), Borogo, etc... for wanted features or greater responsiveness to user demands.
You can introduce data structures and types that utilize enums, with some languages taking that idea further than others, but that's well beyond enums themselves.
You may want to meet a modern language one day. Or hell not even a modern language, even a shit one like Java.
As it turns out there are languages where you can define an
and when you type a parameter as a `foo` you're not at risk of getting 69 or 4328.I agree with your point about Go enums.
But in defense of Java, modern Java is actually pretty pleasant.
Virtual threads, records and sealed classes, pattern matching, state-of-the-art garbage collectors, a great standard library etc. (and obviously well-behaved enums).
Not to mention the other languages you get for free with the JVM ecosystem.
It might not be as expressive as Rust, but certainly Java/JVM > Go.
encoding/json vs
net/http client vs The time library definitely sucks though. There's really no excuse for it, joda-time came out a long time ago.What about actually counting the set of features across a full stack application?
I can pick countless of examples of features in Java and .NET standard library that Go 1.24 still doesn't have any answer for.
Of course, plenty of high-quality third-party libraries exist in the other languages. I've used commons-httpclient and jackson in Java, and everybody and their brother knows about requests in Python (though I prefer aiohttp nowadays). But they are odd omissions from the standard libraries of those languages. At least .NET caught up.
Like what? CrabLang? Its enums are identical to Go, unsurprisingly. Above the enum rests a discriminated union that ends up hiding the details of the enum. That is where things begin to differ greatly. You have to do some real trickery to actually get at the underlying enum result in that language. But, when you do, you see that it is exactly the same.
> and when you type a parameter as a `foo` you're not at risk of getting 69 or 4328.
That's thanks to the type system, though, not enums. Enums are not a type. Enums are a number generator. Hence the name.
What's happened here is that you've mistaken "Things I believe" for "What everybody else believes" but you're talking to other people, not yourself, so, this makes you seem like a blithering idiot.
The particular version of this trap you've fallen into is a variation of the "It's all just the machine integers" mental model from C. It's just a model and the problem arises when you mistake that model for reality.
Now, technically this model isn't even correct for C's abstract machine, but it's close enough for many programmers and it tends to match how they think the hardware works, which is even more confusing for the hardware people who know how it actually works but that's another conversation.
This model is completely useless for languages which don't have the same type system, and so it's no surprise that it immediately led you astray.
No, I am clearly talking to a computer program. It is possible that the program is forwarding the discussion on to people. Perhaps that is what you are trying to allude to? The details of how the software works behind the scenes is beyond my concern. There is no intention to talk to other people, even if the software has somehow created that situation incidentally. If I wanted to talk to people, I would go out and talk to people, not type away at my computer into a box given to me by the software.
> The particular version of this trap you've fallen into is a variation of the "It's all just the machine integers" mental model from C.
As much as I enjoy your pet definition that you've arbitrarily made up on the spot here, the particular trap I have fallen into is the dictionary. It literally states what an enumeration is according to the prevailing usage. It does not describe it as a type, it describes it as the action of mentioning a number of things one by one. Which is exactly what an enum does.
The previous comment is talking about type constraints. You can definitely constrain a type such that it is invalid to use it outside of the numbers generated by the enum, just as you can constrain a type to only accept certain strings. e.g. from Typescript: `type Email = "{string}@{string}"` This idea is not limited to enums.
That's definitely a thing, and definitely a thing that could be used in conjunction with an enum, but is not an enum itself. Not as enum is normally used. Of course you can arbitrarily define it however you please. But, if you are accepting of each holding their own pet definition, your comment doesn't work. You can't have it both ways.
Which, I guess, is why we have this desperate attempt to redefine what an enum is, having become bored with a term that has no invitation potential. But, unfortunately, we have not found shared consensus on what "enum" should become. Some think it should be a constraint, others think it should be a discriminated union, others think a type declaration, so on and so forth.
All of which already have names, which makes the whole thing particularly bizarre. You'd think if someone really feels the need to make their mark on the world by coining "enum" under new usage in the popular lexicon they would at least pick a concept that isn't already otherwise named.
It is true that some type system features built upon enums. Like a previous commenter mentioned, Pascal offers a type that constrains allowable values of that type to be within the values generated by the enumerator. Likewise, I mentioned in another discussion that in CrabLang the enumerator value is used as the discriminant in its discriminated union types, which achieves a similar effect. I expect that confuses some people into thinking types and enums are the same thing, which may be what you are trying to get at, although doesn't really apply here. The difference is known to those reading this discussion.
The biggest problem with this desperate attempt to find new meaning for "enum" is: What are we going to call what has traditionally been known as an enum? It does not seem to have another word to describe it.
https://build-your-own.org/blog/20241125_go_slice_surprise/ goes a bit further, but this is really the point where I decided I wouldn't be able to stand Go.
I think you want Rust.
If all I need is a slice of memory, I'd rather only have to pass around the two pieces of data (pointer and length) than all three.
The growable array type (what you call "vector") is a venerable data type, although it does still have parameters you might reasonable tweak e.g. what's the correct growth factor? Doubling is popular but e.g. Zig chooses new_size = (old_size * 1.5) + 8 and you'll see disagreement about what API should be offered and why - e.g. C++ std::vector has the wrong reservation API
But this thing clearly isn't a mistake, it's intentionally a hybrid, and if it had been a huge success maybe everybody else would scramble to copy it.
A mutable String or Vec has all three fields, but the borrowed slice or str has just pointer and length.
Go chooses to be simpler by not having those kinds of separations, which also makes it a little less efficient under some circumstances.
This type isn't either of those things, it's a hybrid, and Go tries to get away with that because it's a garbage collected language so people don't need to care as much about ownership.
https://go.dev/play/p/J2U4djvMVoY
That can be addressed by passing the slice as a pointer: https://go.dev/play/p/h9Cg8qL9kNL
Slices are passed partly by value (the length), partly by reference (the data).
From your explanation, you would expect that to not mutate the slice passed in, but it does.This can have other quite confusing gotchas, like:
I'm sure the output makes perfect intuitive sense https://go.dev/play/p/79gOzSStTp4I can see why it trips up newcomers, but it feels pretty basic otherwise.
The fact that I can pass a slice to a func 'by value' and mutate the source slice outside the func is already surprising behavior to most people. The fact that it MIGHT mutate the source slice depending on the slice capacity is the part that really drives it home as bad ergonomics for me.
Overall I enjoy working with go, but there are a few aspects that drive me up the wall, this is one of them.
Make it so you can create copy-on-write slices of a larger slice, and a huge number of bugs go away.
Or do what rust did, except at runtime, and keep track of ownership
With of course functions to allow multiple mutable ownership in cases where that's needed, but it shouldn't be the defaultWhile most people highlight the difficulty of picking up the syntax, I find Rust to be an incredibly tedious language overall. Zig has a less expressive type system, but it compiles much faster (though not as fast as Go). I like what Zig and Odin folks are doing over there.
I like the balance Go strikes between developer productivity and power, though I dearly miss union types in Go.
Don't make the fallacy of conflating Rust's slow compile time with its "advanced" (not really, it's 80's tech) type system. Rust compilation is slow for unrelated reasons.
Personally, I write java at my day job and the type system there makes me loooong for rust.
But, it's just a tool, and the tools I choose reflect the type of stuff I want to build. The JVM is extremely impressive in its own right. You're just not going to to find any one runtime or ecosystem that hits every niche. I'm happy to leave the language favoritism to the junior devs—for the vast majority of situations, what you're building dictates which language makes the most sense, not vice versa.
I've been involved in a few successful large scale projects and never felt like the type system of Go is holding me back too much. Sure the error handling could be better, union type would make interface munging easier, but the current state after generics isn't too bad.
Last time I checked, C# had clean and focused syntax for working with collection types. Could you provide an example?
It's just that compile times and DevEx haven't been a priority for most projects.
But sure, LLVM and interfacing with it is quite possibly a big contributor to it.
However, it is actually a good example regarding tooling, as the Haskell ecosystem has interpreters and REPL environments available, for quick development and prototyping, something that is yet to be common among Rustaceans.
Ideally we would be having the F# REPL/JIT, plus Native AOT for deployment, as comparable development workflow experience.
Naturally F# was chosen as example, because that's your area. :)
Not being negative per se, I also would like to have something like Haskell GHCi, or OCaml bytecode compiler, as options on rustup, so naturally something like this might eventually come.
It's true that little has changed, but very little is changing in the data representation of any language, really. Even ones that are evolving rapidly.