Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Go's quite horrendous and limited type system makes it a poor fit for everything. The worst thing about Go is, in fact, the language. Everything except the language redeems it.


I have been programming in Go for several years now and I agree, though I am not that sure even the rest of the ecosystem redeems it that much.

On the other hand, the programming languages used by LLM people seem to be python and javascript mainly.

So while I argue that they all should really move on to modern languages, I think go is still better than the I-can't-even-install-this mess of python and javascript imports without even a Dockerfile that seem to be so prevalent in LLM projects.


Can you elaborate a bit on how does "Go's quite horrendous and limited type system" get in the way of crafting agents?

Honest question, I am genuinely interested in what cannot be done easily or at all due to limitations of the Go type system.


If you want to know only about the type system, nowadays it's mostly the lack of basic enums, a clear divide in basic features of the language and of the libraries (and modern generics support) leading to things like `len(..)` vs `.Len()`. Those actually end up playing a bigger role than it seems imho, but even just the rest is death by a thousand cuts.

You can find many articles on the internet about it, but in my experience I would summarize it in:

It looks like it's made to have a simple compiler, not to simplify the programmer's life.

Initially its simplicity is wonderful. Then you start to notice how verbose things are. Channels are another looks-nice-but-maybe-don't feature. nil vs nil-interface. Lack of proper enums is hurting so much I can't describe it. I personally hate automatic type conversions, and there are so many inconsistencies in the standard and most used libraries that you really start to wonder why some things where even done. validators that validate nothing, half-done tagging systems for structs, tons of similar-but-not-quite interfaces and methods.

It's like the language has learning wheels that you can't shake off or work around. You end up wanting to leave for a better one.

People had to beg for years for basic generics and small features. If google is not interested in it, you'd better not be interested in it and it shows after a while.

Companies started to use it as an alternative to C and C++, while in reality it's an alternative to python. Just like in python a lot of the work and warnings are tied into the linter as a clear workaround. Our linter config has something like 70+ linters classes enabled, and we are a very small team.

C can be described as a relatively simple language (with caveats), C++ has grown to a blob that does and has everything, and while they have lots of footguns I did not find the same level of frustration as with go. You always end up fighting a lot of corner cases everywhere.

Wanted to say even more, but I think I ranted enough.


> Lack of proper enums is hurting so much I can't describe it.

Do you mean sum types? That is not a case of them not being "proper", though. They simply do not exist as a feature at all.

Go's enums function pretty much like enums in every single other language under the sun. If anything, Go enums are more advanced than most languages, allowing things like bit shifts. But at the heart of it all, it's all just the same. Here are enum implementations in both Go and Rust:

[Go] https://github.com/golang/go/blob/f18d046568496dd331657df4ba...

[Rust] https://github.com/rust-lang/rust/blob/40daf23eeb711dadf140b...

While Go leans on the enum value produced by `range` to act as the language's enumerator, while Rust performs explicit incrementing to produce the enumerator, the outcome is no different — effectively nothing more than [n=0, n++]. Which stands to reason as that's literally, as echoed by the dictionary, what an enum is.


Go doesn't even classic type-safe integer-value enums like in C++ or enums.

Yes, you can emulate this style of enums by using iota to start a self-incrementing list of integer constants. But that's not what any language (except for C) has ever meant by "enum".

Enums are generally assumed to be type-safe and namespaced. But in Go, they are neither:

  type Color int

  const (
      Red Color = iota
      Green
      Blue
   )

   func show(color Color) {
       fmt.Printf("State: %v", color)
   }

   fun main() {
       show(Red)
       show(6)
   }
There is no namespacing, no way to — well — enumerate all the members of the enum, no way to convert the enum value to or from a string (without code-genreation tools like stringer), and the worst "feature" of all is that enums are just integers that can freely receive incorrect values.

If you want to admire a cool hack that you can show off to your friends, then yeah, iota is a pretty neat trick. But as a language feature it's just a ugly and awkward footgun. Being able to auto-increment powers of two is a very small consolation prize for all of that (and something you can easily achieve in Rust anyway with any[1] number[2] of crates[3]).

[1] https://crates.io/crates/enumflags2

[2] https://crates.io/crates/bitmask-enum

[3] https://crates.io/crates/modular-bitfield


> Go doesn't even classic type-safe integer-value enums like in C++ or enums.

Sure, but now you're getting into the topic of types. Enums produce values. Besides, Go isn't really even intended to be a statically-typed language in the first place. It was explicitly told when it was released that they wanted it to be like a dynamically-typed language, but with statically-typed performance.

If you want to have an honest conversation, what other dynamically-typed languages support type-safe "enums"?

> But that's not what any language (except for C) has ever meant by "enum".

Except all the others. Why would a enum when used when looping over an array have a completely different definition? It wouldn't, of course. Enums are called what they are in a language because they actually use enums in the implementation, as highlighted in both the Go and Rust codebases above.

Many languages couple enums with sum types to greater effect, but certainly not all. C is one, but even Typescript, arguably the most type-intensive language in common use, also went with "raw" enums like Go.


It's not about 'range', and like you said enum and sum types are tied concepts in other languages, and yes I was talking about sum types.

Even without sum types, there is a common pattern of defining a new type and const-defining the possible values that is a clear workaround on the lack of an 'enum' keyword.

Maybe because the compiler can't be sure that those const values are all the possible values of the type, we can't have things like enforcing exhaustive switches on this "enum", and that is left to the linter at best.

Default-zero initialization is always valid too, which can leave you with an "enum" value that is not present in the const definitions (not everything starts on iota, iota does not mean 0).

It's a hack, it became a pattern. It still is not a proper (or even basic) enum even without sum types.


> It's not about 'range'

It is to the extent that it helps explain what an enum is, and why we call the language feature what we do. Python makes this even more apparent as you explicitly have to call out that you want the enum instead of it always being there like in Go:

    for i, v in enumerate(array):
       # ...
In case I'm not being clear, an array enumerator like in the above code is not the same as a language enumerator, but an array enumerator (or something similar in concept) is how language enumerators are implemented. That is why language enumerators got the name they did.

> It still is not a proper (or even basic) enum even without sum types.

It most certainly is "proper". In fact, you could argue that most other languages are the ones that are lacking. Go's enums support things like bit shifts, which is unusual in other languages. Perhaps it is those other languages that aren't "proper"?

But, to be sure, it's not sum types. That is certain. If you want sum types you are going to have to look elsewhere. Go made it quite clear from the beginning that it wanted to be a "dynamically-typed language with statically-typed performance", accepting minimal static type capability in order to support the performance need.

There is definitely a place for languages with more advanced type systems, but there are already plenty of them! Many considerably older than Go. Haskell has decades on Go. Go was decidedly created to fill in the niche of "Python, but faster", which wasn't well served at the time. Creating another Haskell would have been silly and pointless; but another addition to the long list of obscure languages serving no purpose.


> Companies started to use it as an alternative to C and C++, while in reality it's an alternative to python. Just like in python a lot of the work and warnings are tied into the linter as a clear workaround. Our linter config has something like 70+ linters classes enabled, and we are a very small team.

I thought the main "let's migrate our codebase to Go" crowd had always been from the Java folks, especially the enterprise ones. Any C/C++ code that is performant is about to get a hit, albeit small, from migrating to a GC-based runtime like Go, so I'd think that could be a put off for any critical realtime stuff - where Rust can be a much better target. And, true for both C++ and Java codebases, they also might have to undergo (sic) a major redux at the type/class level.

But yes, the Googlers behind Go were frustrated by C++ compile times, tooling warts, the 0x standard proposal and concurrency control issues - and that was primal for them, as they wanted to write network-server software that was tidy and fast [1]. Java was a secondary (but important) huge beast they wanted to tackle internally, IIRC. Java was then the primary language Googlers were using on the server... Today apparently most of their cloud stuff is written in Go.

[1] https://evrone.com/blog/rob-pike-interview


Well, there's a difference between "our program is written in C++ because we correctly chose it for its performance" and "our program happens to be written in C++ because some programmer 10 years ago really liked C++".

There's a lot of software out there that either was written before good modern options existed, or uses very outdated patterns, or its language wasn't chosen with much thought.


It's just a uninformed hivemind comment written by someone lacking original thought.

If you are interested in the merits of golang, you should listen to someone who uses it.


I used it for years.


I think the point was that "Go's quite horrendous and limited type system" gets in the way of everything (programming in general), nothing specific to crafting agents.

There's a lot of discussions on the internet about the bad design decisions of Golang (for example around channels, enums, error handling, redeclarations, interfaces, zero values, nilability... at least generics aren't so much a subject anymore)


To be fair, it is unlikely that Go would have a static type system at all if they had figured out how to achieve the performance expectations without. It was made abundantly clear that it is intended to be like a dynamically-typed language, but faster. Thinking of it as being a statically-typed language is a bit flawed, and shows a gross misunderstanding of what the language was created for.

While you could try to argue that dynamically-typed languages in general are a poor fit for everything, the reality is that people there are typically using Python instead – and the best alternative suggestions beyond Go are Erlang and Elixer, which are also dynamically typed, so that idea doesn't work. Dynamic typing is what clearly fits the problem domain.


I agree- multiple return values don't compose; errors are better than exceptions, but still super verbose; channels have a lot of foot guns; enums are just sad.

But despite all of that the language has some really good qualities too- interfaces works far better than it feels like they should; the packaging fits together really well (I'm learning Rust right now and the file structure is far more complicated); and people are able to write a lot of linters/codegen tools BECAUSE the language is so simple.

All in all I worry the least about the long term maintenance cost of my Go code, especially compared to my Python or JS code.


> enums are just sad.

There isn't much more you can do with them. Literally all an enum can produce is a number.

In increasingly common use in a number of languages, enums are being coupled with discriminated unions, using the enum value as the discriminant. This is probably what you're really thinking of, noticing Go's lack of unions.

But where you might use a discriminated union if it were available, you would currently use an interface, where the type name, rather than an enum, is what differentiates the different types. If Go were to gain something like a discriminated union, it is likely it would want to extend upon that idea. Most especially given that generics already introduced syntax that would lend itself fairly well to it:

    type Foo interface {
        Bar | Baz
    }
Where enums are used to act as a discriminant in other languages, that is largely just an implementation detail that nobody really cares about. In fact, since you mentioned Rust, there are only 8,000 results on Github for `std::mem::discriminant` in Rust code, which is basically nothing. That is quite indicative that wanting to use an enum (in Rust, at least) is a rare edge case. Funny, given how concerned Rust users seem to be about enums. Talk is cheap, I suppose.


Java has true enums that are neither fancy integers nor discriminated unions. The following is not a list of integers:

    public enum Day {
        SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
        THURSDAY, FRIDAY, SATURDAY 
    }
To use this enum, you typically declare a variable of type Day, which is a subclass of Enum, itself a subclass of Object, which cannot be cast to or from int. If a variable is typed as Day, then it can only take one of these variants (or null). Even though the Day class does have an ordinal() method, and you can look up the variants by ordinal, you cannot represent Day(7) or Day(-1) in any way, shape, or form. This sealed set of variants is guaranteed by the language and runtime (*). Each variant, like SUNDAY, is an instance of class Day, and not a mere integer. You can attach additional methods to the Day class, and those methods do not need to anticipate any other variants than the ones you define. Indeed, enums are sometimes used with a single variant, typically called INSTANCE, to make true singletons.

* = There is a caveat here, which is that the sealed set of variants can differ between compile-time (what's in a .java file) and runtime (what's in a .class file) but this only happens when you mismatch your dependency versions. Rather importantly, the resolution of enum variants by the classloader is based on their name and not their ordinal, so even if the runtime class differs from the compile-time source, Day.MONDAY will never be turned into a differently named variant.


> The following is not a list of integers

Then I am not sure how you think it is an enum? What defines an enum, literally by dictionary definition, is numbering.

It is hilarious to me that when enum is used in the context of looping over an array, everyone understands that it represents the index of the element. But when it comes to an enum in a language, all of a sudden some start to think it is magically something else? But the whole reason it is called an enum is because it is produced by the order index of an AST/other intermediate representation node. The very same thing!

While I haven't looked closely at how Java implements the feature of which you speak, I'd be surprised if it isn't more or less the same as how Rust does it under the hood. As in using a union with an enumerator producing the discriminant. In this case it would be a tag-only union, but that distinction is of little consequence for the purposes of this discussion. That there is an `ordinal` method pretty much confirms that suspicion (and defies your claim).

> you cannot represent Day(7) or Day(-1) in any way, shape, or form.

While that is true, that's a feature of the type system. This is a half-assed attempt at sum types. Enums, on the other hand, are values. An enum is conceptually the same as you manually typing 1, 2, 3, ... as constants, except the compiler generates the numbers for you automatically, which is what is happening in your example. The enum is returned by `ordinal`, like you said. Same as calling std::mem::discriminant in Rust like we already discussed in a sibling thread.


The existence of the ordinal method reveals nothing except that the ordinal exists. It can be (and is) simply a field on each Day object, not an index into anything (though the Day objects are probably stored in an array, this is not required by any property of the system). Day.SUNDAY is ultimately a pointer, not an int. It is also a symbolically resolved pointer, so it will never become Day.MONDAY even if I reorder the variants so that their ordinals are swapped. The ordinal is not a discriminant.

You seem to be trivializing the type system. This property is not imagined solely by the compiler, it is carried through the language and runtime and cannot be violated (outside of bugs or unsafe code). Go has nothing like this.

If you choose to call this "not an enum", that is certainly your idiosyncratic prerogative, but that doesn't make for very interesting discussion. Even though I agree that discriminated unions aren't enums and am somewhat annoyed by Rust's overloading of the term, this is not that.


> The existence of the ordinal method reveals nothing except that the ordinal exists.

It strongly suggests that implementation is a discriminated union, just like Rust. Again, it is tag-only in this case, where Rust allows also attaching payload, but that's still a type of discriminated union. That it is a set of integers – contrary to the claim made earlier – combined with you explaining how the compiler treats it like a discriminated union — as in that there are type checks against the union state, that does reveal that it couldn't be anything other than a discriminated union that is effectively identical to what we find in Rust, along with many other languages these days.

> It can be (and is) simply a field on each Day object, not an index into anything

So...? Enum is not staunch in exactly where the number comes from; it simply needs to number something. Indices are convenient, though, and I am not sure why you would use anything else? That doesn't necessarily mean the index will start where you think it should, of course.

For example,

    enum Foo { A, B, C }
    enum Bar { X, Y, Z } 
In some languages, the indices might "reset" for each enum [A=0, B=1, C=2, X=0, Y=1, Z=2], while in other languages it might "count from the top" [A=0, B=1, C=2, X=3, Y=4, Z=5]. But, meaningless differences aside, where else is the number going to come from? Using a random number generator would be silly.

But, humour us, how does Java produce its enums and why doesn't it use indices for that? Moreover, why did they choose to use the word `ordinal` for the method name when that literally expresses that it is the positional index?


Setting aside the full enum API, as well as certain optimizations, this is a rough equivalent of the enum I gave:

    public class Day extends Enum<Day> {
        private int _ordinal;
        private Day(int ordinal) { this._ordinal = ordinal; }
        public int ordinal() { return this._ordinal; }
        public static final Day SUNDAY = new Day(0);
        // ...
        public static final Day SATURDAY = new Day(6);
    }
with the added constraint that the Day constructor cannot be invoked by reflection, and the static instances shown herein can be used in a switch statement (which may reduce them to their ordinals to simplify the jump table). Each instance is ultimately a pointer, so yes it could be pulled from a sort of RNG (the allocator). As I said they are probably in an array, so it's likely that the addresses of each variant start from some semi-random base but then increase by a fixed amount (the size of a Day object). A variable of type Day stores the pointer, not the ordinal.

Now, it really seems to be in the weeds of pedantry when you start talking about discriminated unions that have only discriminants and no payload. Taking from your examples, the key point is that a Foo is not a Bar and is also not an int. Regardless of whether the variants are distinct or overlapping in their ordinals, they are not interchangeable with each other or with machine-sized integers.


> this is a rough equivalent of the enum I gave

Yes, this echos what I stated earlier: "An enum is conceptually the same as you manually typing 1, 2, 3, ... as constants, except the compiler generates the numbers for you automatically" Nice to see that your understanding is growing.

> Taking from your examples, the key point is that a Foo is not a Bar.

I'm not sure that's a useful point. Nobody thinks

   class Foo {}
   class Bar {}
...are treated as being the same in Java, or, well any language that allows defining types of that nature. That is even the case in Go!

    type Foo int
    type Bar int

    const f Foo = iota
    const b Bar = f // compiler error on mismatched types
But what is significant to the discussion about enums is the value that drives the inner union of the class. As in, the numbers that rest beneath SUNDAY, MONDAY, TUESDAY, etc. That's the enum portion.


I don't understand anything more now than I did at the start. It is clear we are talking past each other.

The values of Day are {SUNDAY, ..., SATURDAY} not {0, ..., 6}. We can, of course, establish a 1:1 mapping between those two sets, and the API provides a convenient forward mapping through the ordinal method and a somewhat less convenient reverse mapping through the values static method. However, at runtime, instances of Day are pointers not numbers, and ints outside the range [0, 6] will never be returned by the ordinal method and will cause IndexOutOfBoundsException if used like Day.values()[ordinal].

Tying back to purpose of this thread, Go cannot deliver the same guarantee. Even if we define

    type Day int
    const (
        Sunday Day = iota
        // ...
        Saturday
    )
then we can always construct Day(-1) or Day(7) and we must consider them in a switch statement. It is also trivial to cast to another "enum" type in Go, even if the variant doesn't exist on the other side. This sealed, nonconvertible nature of Java enums makes them "true" enums, which you can call tag-only discriminated unions or whatever if you want, but no such thing exists in Go. In fact, it is not even possible to directly adapt the Java approach, since sealed types of any kind, including structs, are impossible thanks to new(T) being allowed for all types T.


> This sealed, nonconvertible nature of Java enums makes them "true" enums, which you can call tag-only discriminated unions or whatever if you want, and no such thing exists in Go.

It is no secret that Go has a limited type system. In fact, upon release it was explicitly stated that their goal was for it to be a "dynamically-typed language with statically-typed performance", meaning that what limited type system it does have there only to support the performance goals. You'd have to be completely out to lunch while also living under a rock to think that Go has "advanced" types.

But, as before, enums are values. It is not clear why you want to keep going back to talking about type systems. That is an entirely different subject. It may be an interesting one, but off-topic as it pertains to this discussion specifically about enums, and especially not useful when talking in the context of Go which it isn't really intended to be a statically-typed language in the first place.


#[repr(u8)]

enum Discriminant {

Disc0 = 0,

Disc1 = 1,

}


Funny enough, manually defining the discriminant disables the enumerator:

    enum Discriminant1 {
        Disc0,
        Disc1,
    }

    #[repr(u8)]
    enum Discriminant2 {
        Disc0 = 10,
        Disc1 = 20
    }

    fn main() {
        let d1 = Discriminant1::Disc1;
        let d2 = Discriminant2::Disc1;
        println!("{:?}", std::mem::discriminant(&d1)); // Value by enumerator.
        println!("{:?}", std::mem::discriminant(&d2)); // Value by constant.
    }
Which makes the use of the enum keyword particularly bizarre given that there is no longer even an enumerator involved, but I suppose bizarre inconsistencies are par for the course in Rust.


What's that have to do with Rust though? Rust takes it straight from C: https://godbolt.org/z/Ysb6M66h4

And because it has been used like that in C for decades, the dictionary definition takes a backseat to the now de-facto C-based definition (at least for popular systems languages, which Rust is trying to share as much syntax with).


> Rust takes it straight from C

Meaning the keyword? Sure, C has the same inconsistency if you disable the enumerator with manual constant values. C is not exactly the paragon of thoughtful design. But whataboutism is a dumb path to go down.

> the dictionary definition takes a backseat to the now de-facto C-based definition

That's clearly not the case, though, as the functionality offered by the Rust enum keyword is very different. It puts absolutely no effort into being anything like C. Instead, it uses enum as the keyword for defining sum types. The C enum keyword, on the other hand, does nothing but define constants, and is functionally identical to what Go has. There is an enum involved in both cases, as demonstrated earlier, so the terminology isn't strictly wrong (in the usual case) but the reason for it existing shares little commonality.

But maybe you've moved onto the concept of enums rather than syntax and I didn't notice? You are right that the dictionary definition is in line with the intent of the C keyword, which speaks to the implementation, and is how C, Rust, Go, and every other language out there use the terminology. In another comment I even linked to the implementation in both Go and Rust and you can see that the implementation is conceptually the same in both cases: https://news.ycombinator.com/item?id=44236666


I wouldn't mind a well-maintained LISP/Scheme dialect that compiled to Go.


Definitely not well maintained, but it's interesting to see that something like that came out of SteelSeries:

https://techblog.steelseries.com/golisp/index.html

https://github.com/SteelSeries/golisp

I wonder if they still use it.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: