Rendered at 17:04:20 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
YesThatTom2 2 hours ago [-]
"The best way to teach something new is to compare it to something the audience already understands."
Could someone take the example, reduce it to a non-generic version for two types I DO understand, then show that with the new feature I can collapse them into the Box/Map example in the doc?
I have 10+ years of Go experience and I can't make heads or tails of "(b Box[T]) Map[U any](f func(T) U) Box[U]"
pkal 12 minutes ago [-]
If you instantiate it with concrete types, does "(b IntBox) Map(f func(int) string) StringBox" make more sense? You have a collection (in this case Box) containing values of type T, a function that maps values of type T to type U, and if you apply that function to all elements in that collection you get a collection of type U.
typical182 1 hours ago [-]
It's not a great example.
I think it's trying to show a mapping operation for a generic container where the container values are of one type and the mapping function is allowed to return a container with values of a different type.
type MySlice []int
// Map maps from a slice of ints to a slice of float64s.
func (s MySlice) Map(f func(int) float64) []float64 {
var out []float64
for i := range s {
out = append(out, f(s[i]))
}
return out
}
From a quick search, this seems to be better explanation of this new 1.27 feature:
(That uses an example that seems similar in spirit to the Interactive Tour's example, but with a more useful type of a Stack[T] and corresponding explanation seem clearer.)
typical182 16 minutes ago [-]
And to answer the second half of your request, here is that exact same code as above, but now using the 1.27 generic methods feature (with a runnable example using tip at https://go.dev/play/p/1YK62tGetsm?v=gotip):
// Map maps from a slice containing type In to a slice containing type Out.
func (s MySlice[In]) Map[Out any](f func(In) Out) []Out {
var out []Out
for i := range s {
out = append(out, f(s[i]))
}
return out
}
In short, you could always have methods on a generic type since Go first introduced generics in Go 1.18, but with 1.27, the methods on the generic type can also introduce their own additional type parameters.
(Previously, you could achieve the same net effect with a top-level generic function, but then the code would not be grouped as nicely as hanging it off of the type, and arguably it now can have slightly better ergonomics in some cases. You can see more of the rational from Robert Griesemer at https://github.com/golang/go/issues/77273.)
LukeShu 2 hours ago [-]
type IntBox struct { v int }
type StrBox struct { v string }
func (b IntBox) MapToStr(f func(int) string) StrBox {
return StrBox{v: f(b.v)}
}
(Please forgive any typos I made on mobile.)
It wasn't a great example because "Box" isn't really a useful type. But the point is that you no longer need to define a separate "MapToXXX" method for every type you might want to map to; now you can have just one type-generic "Map" method.
2 hours ago [-]
baalimago 10 hours ago [-]
This: "(b Box[T]) Map[U any](f func(T) U) Box[U]" is the type of cognitive weight I was happy that Go avoided.
fauigerzigerk 9 hours ago [-]
It's hard to avoid, because (naming aside) the cognitive load is caused by higher order functions, which are hard avoid without causing massive code duplication.
I understand the desire to keep things concrete and avoid high level abstractions, but it's a decision not to automate stuff that can easily be automated. It runs counter to the basic instincts and purpose of our field/industry. That's why it never sticks.
hnlmorg 7 hours ago [-]
Honestly, I’ve written some applications that, on paper, should be the perfect candidate for generics. And yet I can still count on one hand the number of times generics have saved me from massive code duplication.
Most of the time generics might be useful, I’ve ended up needing reflection too anyway. And at that point, I’m really no better off for generics.
fauigerzigerk 6 hours ago [-]
I understand that this is true for a lot of application code. It's not true for library authors though, and every language needs libraries.
hnlmorg 5 hours ago [-]
I’ve written a lot of libraries too.
The problem is generics only solve a very small part of the equation: compile time checks for composite types. But to use composite types in anything non-trivial in Go, you then need reflection. Which is slow. And if you then need reflection, you’re already passing interface types anyway plus you’re back to having to handle type-handling errors in the runtime.
So if you’re writing a library that’s expected to have any kind of performance, you’re back to code duplication and having a DoSomethingType() function signatures again.
Or you stick with reflection and take that performance hit PLUS the risk of compile time constraints being runtime errors; which is the a lose-lose scenario. And let’s also not forget that reflection can be just as verbose as code duplication, and harder to get right too.
Don’t get me wrong, I’m glad we have generics. But people on HN massively overstate the value of them in a AOT non-dynamic, strictly typed language like Go.
I guess you could argue that Go has other shortcomings that directly result in generics having limited value. But then you’re basically just arguing that you prefer coding in a different language paradigm, and at that point, you’re much better off using that other paradigm instead of complaining that Go isn’t JavaScript or Haskell.
theptip 2 hours ago [-]
Interesting, thanks - is the problem you’re describing solved by Rust’s macros (eg derive) or are there further issues you see even there?
pezo1919 2 hours ago [-]
Thanks for that, nicely put, interesting angle.
Pay08 8 hours ago [-]
Lisp manages it. Even if you do use type annotations.
fauigerzigerk 8 hours ago [-]
No. This cognitive load is conceptual. You can't avoid it by using slightly different syntax.
stingraycharles 2 hours ago [-]
I think the problem is that the Box / Map / any stuff and all the explicit declarations that Go requires makes it harder.
In Haskell as well, you can let the compiler infer a lot of things but that doesn’t appear to be the case with this example.
I’d want the compiler to infer things, but that - I think - is at odds with Go desiring a fast compiler, which I also understand.
Pay08 35 minutes ago [-]
I was about to mention Haskell as well, I feel like it also avoids this sort of cognitive load. Maybe it's something to do with the languages being designed as functional languages instead of languages with functional components.
public <OutType> Box<OutType> map(Function<InType, OutType> transformFunction)
vs.
public <U> Box<U> map(Function<T, U> f)
zerr 3 hours ago [-]
We often forget that our profession (computer programming) belongs to STEM. Some (like Go 1.0 :)) wish to think it is Arts & Humanities. The sooner we realize that yes, it is OK and actually expected to bear a cognitive weight of "(b Box[T]) Map[U any](f func(T) U) Box[U]" the sooner we get back to reality... :)
wannabe44 31 minutes ago [-]
In terms of tooling, Go is one of the few languages which remembers that we are in STEM.
red_admiral 3 hours ago [-]
Just because we can, doesn't mean we have to. I'd prefer to have some more brain-cache free to concentrate on the problem I'm trying to debug rather than doing type resolution in my head.
yladiz 2 hours ago [-]
Please. I’m sorry, but you kind of can’t avoid needing to think about types unless you use a language like JavaScript which is super loose with its type conversions, and you especially can’t avoid in a language like Go. With generics in Go you don’t even need to prefill the types like you go with a lot of other cases, so I’m dubious about the cognitive overhead.
thebytefairy 3 hours ago [-]
People should stop using these simplified high level programming languages with low cognitive weight, like Go. I only write assembly. ;)
4ndrewl 2 hours ago [-]
(b Box[T]) Map[U any](f func(T) U) Box[U] _is_ for the Arts and Humanities.
Unless you're writing assembler in vim you're not STEM.
fragmede 2 hours ago [-]
Why should I, as fallible human of limited short and long term memory, bear that cognitive weight when I have a perfectly good compiler on a computer to offload that particular cognitive weight to?
adrianmsmith 10 hours ago [-]
I never understood the convention of using single letter names for generic parameters. I guess this started in C++ and every language has copied that convention.
I think that code would be a lot easier to read if the types were called IN and OUT or In and Out or TIn and TOut or something like that.
jiehong 9 hours ago [-]
We all know letters are expensive ^^
toinebeg 7 hours ago [-]
I often use whole word for type annotation, when I can find meaningfull ones. I just type them in all caps to stay close to the convention.
I guess the single letter thing is laziness for a part. It's not simple to find words that represent the abstract idea behind the generic type without narrowing the possibilities. For array function, the Key Value from the sibling comment work but for more complex use case, it get complicated.
wwalexander 6 hours ago [-]
Swift generics tend to idiomatically use longer names, like Element or View or Content.
girvo 5 hours ago [-]
I’ve always done that in my typescript code bases too, and I’ve never regretted it
fooooor 2 hours ago [-]
Lambdas usually have short variable names because the scope is small, typically half a line. And that is fine, even optimal.
spockz 9 hours ago [-]
Completely agree and I personally name generic type parameters as I would name types and parameters. It helps a lot.
fooooor 2 hours ago [-]
What could be more idiomatic than:
for (int i=0; i<10; i++) {
printf(”%d\n”, i);
}
(Or the very similar Go equivalent)
If you having a hard time parsing that, due to the short variable name, i.e. if it’s a huge cognitive load for you, I suggest you switch career, b/c the IT industry is obviously not a good fit.
With that said, Go is explicit with suggesting short variable names for small scopes, and long variable names for bigger scopes. This a good practice in all languages.
Someone 8 hours ago [-]
For maps, a convention is to use K and V for key, respectively Value.
I think that’s best as you’ll soon learn the “single-character capital letter ⇒ generic parameter” convention
Laurel1234 8 hours ago [-]
In C# this is the convention.
eterm 4 hours ago [-]
It's a mix, because some stuff tends to just use `T`, but there's better descriptors elsewhere.
There's IList<T> but Task<TResult>
There's Action<T1, T2, T3, T4, T5, T6> but also Dictionary<TKey, TValue> and Map<TIn, TOut>
This stuff kind of "makes sense" once you're used to it, because it's difficult to say what IList<T> ought to have been called otherwise, IList<TContainee> is a mouthful, and Action<T1,...> simply suffers from the inability to specify an unknown number of generic parameters.
Im pretty sure it came from the MLs, where you usually have a/b/c instrad of the T,U etc combo.
I dont find it confusing, as its pretty clear that it only an placeholder.
In generics the name usually does not matter or is REALLY hard to name so that it makes sense.
More specifically in Go where you have interfaces, concrete types and generics.
asQuirreL 8 hours ago [-]
Fairly sure it would predate even that, and go all the way to lambda calculus, and predicate logic before that, and that's where my knowledge stops and somebody else can tell us where the current conventions around variables in logic and mathematics come from.
teh64 6 hours ago [-]
In ocaml (and I assume SML) it helps that the generic types have a `'` before them, so
val map : ('a Box) -> ('a -> 'b) -> 'b Box
setopt 8 hours ago [-]
I believe Haskell did that for decades before C++.
logicchains 7 hours ago [-]
Haskell was created in 1990, five years after C++.
hnlmorg 7 hours ago [-]
Haskell had generics from the beginning. Whereas C++ only added templates to its specification in 1998.
zerr 2 hours ago [-]
Miranda had it since its release in 1985.
jahnu 3 hours ago [-]
I was using the STL in 1994 with Zortech C++
dvdkon 8 hours ago [-]
Maybe it's just familiarity, but I think it would look a lot more comprehensible with some punctuation. Just because a syntax is formally unambiguous doesn't mean it looks that way to humans.
It's not good Go code anyway. In Go you would use a for loop. Just because Go has generics nowadays doesn't mean you should abandon good taste and write ML/Haskell/Rust/C# in it.
wannabe44 29 minutes ago [-]
Sure it's a bad example. But you can't simplify something like this without losing type safety:
SortBy[T, K comparable](slice: []T, key: func (T) K)
red_admiral 3 hours ago [-]
Indeed, we're now one step away from monads. I know https://go.dev/doc/effective_go hasn't been updated for while, but it also seems to have been forgotten. "Go is an open-source programming language that focuses on simplicity ..." the page begins.
treyd 3 hours ago [-]
You've already been able to badly implement monads in Go for 10+ years. Why wouldn't you be able to implement them in a way that the compiler can enforce correctness of?
If you don't want it don't use it. It's that simple.
fooooor 2 hours ago [-]
No it’s definitely not that simple. Code are read more often than written, no one works in a vacuum, especially not in open source. Also, when in Rome...
golem14 2 hours ago [-]
If a project's owner feels that strongly about not using generics, that's a choice. No dependencies using generics, no generics allowed in PRs etc. Perfectly doable.
Also, screw those Romans ;)
inigyou 3 hours ago [-]
Apparently the people responsible for the simplicity retired. Since it's Google, some new people want to be promoted for adding features to Go.
twsted 9 hours ago [-]
I really understand your feeling, I escaped from C++ years ago when I was overwhelmed by meta programming (initially i loved it).
But anyway I find this in Go much more bearable.
zerr 2 hours ago [-]
As a C++ (including modern) developer for more than 20 years, I had written a "template" keyword only for a handful of times. Maybe once in every 5 years on average... :)
Unless you are a compiler/stdlib vendor or contributing to Boost, there are features that you just don't use it daily.
skywhopper 2 hours ago [-]
Is it worse than having to create endless functions for each type pair?
The T, U, and f names are the cognitive load here, because they are meaningless variables. For a specific solution, those would have meaningful names that would make it easier to understand.
kitd 9 hours ago [-]
It looks more reasonable (literally lol) with syntax highlighting though.
fooooor 2 hours ago [-]
Luddites don’t use syntax highlighting though.
HumblyTossed 4 hours ago [-]
Right? Sigh. I really dislike this.
There are 37000 programming languages, stop forcing every single one that gets popular to look like this.
scotty79 7 hours ago [-]
> (b Box[T]) Map[U any](f func(T) U) Box[U]
Map method
of b (of type Box[T])
that takes f
(of type function that takes value of type T and returns value of type U (which could be any type))
and returns value of type Box[U]
is defined as follows
return Box[U]{v: f(b.v)}
func[U any] b:Box[T].Map(f:func(T)->U)->Box[U]:
return {v: f(b.v)}
func[U any] Box[T].Map(f:func(T)->U)->Box[U]:
return {v: f(this.v)}
// maybe all of the types could be inferred from usage?
func Box[].Map(f):
return Box[]{v: f(this.v)}
Eh... I think you'd need to avoid generics altogether.
wannabe44 28 minutes ago [-]
Map/Filter/Reduce is a bad example for an imperative language. But look at slices and maps packages, or the new proposal for container types. There are many good examples how generics are like salt to food. Another example is errors.AsType.
inigyou 3 hours ago [-]
that's literally what Go was supposed to do! If I want a language like C++, I know where to find a language like C++ (it's C++).
chenxiaolong 15 hours ago [-]
This release also fixes runtime.findnull() to be compatible with MTE on Android ([1] and [2]). This was the only thing preventing MTE from being enabled for apps that use gomobile on MTE-compatible Android OS's like GrapheneOS.
I still don’t understand why Go isn’t the primary supported language for android.
mappu 14 hours ago [-]
Automatically draining http response bodies is a risky silent behaviour change. I think it will be an improvement for most applications, but it's very subtle if you were relying on the old behaviour
kune 10 hours ago [-]
The Go team is addressing that in the release notes: https://go.dev/doc/go1.27 They think it will only affect use cases where a high number of idle connections were allowed to linger, for instance by setting MaxIdleConns in Transport to 0. They recommend to disable keep alives in that case.
gigatexal 10 hours ago [-]
Can you go more into this? I don’t quite follow
mappu 10 hours ago [-]
Go's http.Client will keepalive a TCP/TLS connection to save you handshake latency on second requests. But it can only do this if you completely finish reading the last request.
Now in 1.27:
> http.Response.Body drains itself on Close. For HTTP/1, closing the body now reads and discards any unread content (up to a conservative limit) so the connection can be reused. For most programs this is a transparent win [...]
Great, so i no longer have to io.Copy(io.Discard, resp.Body) in the err case, one less thing to worry about; but
> if you were leaning on an early Close to abort a large download, set Transport.DisableKeepAlives to opt out.
That's a subtle behaviour change. Any previous Go program which used Close in this way - say for an infinite event stream - now hangs, soaking up bandwidth.
In the past, the Go team have searched the entire Github corpus for misuse before making changes like this. I don't have a reference but I assume an appropriate level of consideration went into this decision.
EDIT: ""up to a conservative limit"" so this is not so bad after all.
spockz 9 hours ago [-]
Is “a conservative limit” a high limit or a low limit? If it is high such that many responses will still be drained it would keep reading those infinite streams for a long time. If it is low it might still not drain all normal sized messages.
Anyway, this is why it pays off to read release notes closely and have a decent test suite.
mxey 6 hours ago [-]
The drain is asynchronous, so it won’t block.
The limit is 256 KB and 50 ms.
MartinodF 9 hours ago [-]
Say you have some code that does a request to an HTTP/1 dependency, and if it get an error response, just closes the connection without reading the response body.
Go 1.26 in practice never re-used that connection, it always established a new one because you can't reuse a connection which has a pending response ready to be read.
Go 1.27 will now consume the body for you, causing your application to re-use connections much more aggressively, bringing in potential edge cases (e.g. dependency is broken, connection is now permanently unusable, your app no longer recovers automatically).
To be clear, I'm very glad for the change and I had equivalent code in our in-house framework to do just that, but yeah it does change the behavior in a way that it could expose undetected issues.
sbstp 12 hours ago [-]
Go's standard library has always been it's strength, especially the crypto package! Lovely stuff.
my-next-account 10 hours ago [-]
>The quieter but bigger change
I really wish they didn't use such stupid LLM-isms.
neilprosser 9 hours ago [-]
I do wonder whether, as a group of people being regularly exposed to text written by LLMs, we'll gradually end up writing and talking like that in our normal language. At that point perhaps text written by LLM and human will be indistinguishable. I already find myself using terms like 'footgun' in jest more than I ever did before!
"The creatures outside looked from pig to man, and from man to pig, and from pig to man again; but already it was impossible to say which was which."
― George Orwell, Animal Farm
yladiz 5 hours ago [-]
The percentage of people that use LLMs so much that their language would change based on its responses is small enough that I’d doubt it would happen. Maybe for technical groups that use it more, but not for the general population.
zer00eyz 3 hours ago [-]
> I do wonder whether, as a group of people being regularly exposed to text written by LLMs, we'll gradually end up writing and talking like that in our normal language.
We were doing this before LLM's. All sorts of trendy business speak would spread - the term "synergy" springs to mind as one people beat to death.
This is the concept of "memetics" (as in meme) in action. Hank Green recently talked about using AI and he went "off script" and threw "I appreciate the pushback" into his speech on the fly...
LLM's generating large volumes of content means that we're going to see all of its "isms" creep into other peoples speech much faster.
abtinf 7 hours ago [-]
The entire thing is obviously LLM generated. Much better off just reading the release notes.
jonathrg 7 hours ago [-]
Worth flagging, real gap, transparent win, center of gravity... I'm tired boss
nasretdinov 4 hours ago [-]
Now I have full understanding
jonathrg 28 minutes ago [-]
This is the key insight.
aaa_aaa 7 hours ago [-]
This post seems to be mostly llm generated
ewy1 2 hours ago [-]
fascinating how many people are displeased with the expansion of generics! i love go and this is some functionality i always missed.
nu2ycombinator 14 hours ago [-]
Those Generics syntax in Golang seems so hard to read.
trueno 4 hours ago [-]
stared at it for a bit and im mostly certain i prefer it to java. at least writing other go = its not bad for me to break apart the signature line on a generic
java feels kinda unhinged the more that i look at it
public static <T extends Comparable<? super T>> T max(Collection<? extends T> c)
:x i wonder if anyones done something like this, would be super unhinged
go seems to get a lot of flack around these parts. i kinda lurv it though, just getting compiled binaries out of not much code and not needing a runtime to do shtuff. once i got a wrangle on goroutines i dunno i feel like its pretty solid for webapp backend which is mostly what i use it for
theplumber 13 hours ago [-]
It is verbose but inference helps a lot to keep it “tidy”. I always find myself increasing my focus a notch when I start dealing with generics. It’s one of the things I use only if I really “need”.
xmprt 11 hours ago [-]
One of the reasons it took so long to implement generics in Go was because there was a lot of stuff you could do that didn't need it. Now that generics are there, a lot of that stuff is still the best way to solve the problem and many of the methods that require generics are in the standard library so it's rare that you absolutely need it.
bessel-dysfunct 12 hours ago [-]
Yeah, I agree with that.
Back when Go's generics came out, I was working with about 20% Go and 80% Python. I looked at the syntax, went "not today, Satan" and never bothered to learn it. For the past half year I've been in a mode where most of my coding time is spent with Go and I've been completely indoctrinated. I unironically like thinking about how generics and interfaces interact now.
I also need to slow down when I need to use them for non-trivial stuff. Not just relative to Go code but relative to how much I needed to think about them back when I was using OCaml. I think part of it is that I save them for hard issues and use interfaces for easy stuff.
Cthulhu_ 8 hours ago [-]
It does, but at the same time it's not "normal" code; I see it much like Typescript's advanced types, ultimately it's something that mainly lives in libraries.
twsted 9 hours ago [-]
imho it is much better that C++ equivalent
neild 11 minutes ago [-]
Many comments on generic methods. Perhaps this example will help understand a hopefully not-too-objectionable case of using them in practice.
The math/rand/v2 package has a number of functions which return random numbers of a certain type:
i := rand.Int32() // a random signed 32-bit integer (type int32)
j := rand.Uint64() // a random unsigned 64-bit integer (type uint64)
It has functions which return a number within a range:
in := rand.Int32N(10) // a random int32 in the range [0,10)
jn := rand.Uint64N(100) // a random uint64 in the range [0,100)
It also has a generic function, rand.N, where the return type is set by a type parameter. The definition of rand.Int32N (for comparison) and rand.N are:
func Int32N(n int32) int32
func N[Int intType] (n Int) Int
Adding some spaces to make the common elements align (apologies if my formatting gets mangled), that's:
func Int32N (n int32) int32
func N [Int intType] (n Int ) Int
As you can see, the generic function N has the same signature as the non-generic Int32N, except the type it operates on is set by a type parameter (named "Int"). The type parameter has a constraint, intType, which is a private type defined in the math/rand package. (There's nothing magic about this constraint, it's just a list of all the integer types in the language, and you can write it yourself if you want to. It's a separate type to keep the function signature of N from becoming too large, and it's internal to math/rand because it doesn't need to be part of the public package API.)
The nice thing about rand.N is that it lets you write something like this:
// d is a random time.Duration in the range [0, 10 minutes)
d := rand.N(10 * time.Minute)
Without generics, you'd instead write this as the following, which is a lot more noise:
d := time.Duration(rand.Int64N(int64(10 * time.Minute)))
The generic rand.N has a more confusing type signature and a lot more language complexity behind it, but the code using it is simpler and easier to read. We think that's a good tradeoff, but of course not everyone will agree.
All the functions I've mentioned so far use a default random number source. Each of them also exists as a method of the rand.Rand type, which generates numbers from a user-provided randomness source. For example:
rng := rand.New(rand.NewChaCha8(seed))
a := rng.Uint64()
b := rng.Uint64N(100)
There is one exception, though: Until Go 1.27, there was no Rand.N method, because we did not support generic methods. (A generic type could have methods, but those methods could not be further type parameterized.)
In Go 1.27, there is now a Rand.N method:
// Using a ChaCha8-based source with a defined seed,
// generate a duration in the range [0, 10 minutes).
rng := rand.New(rand.NewChaCha8(seed))
d := rng.Duration(10 * time.Minute)
This method's signature is:
func (r *Rand) N[Int intType](n Int) Int
Comparing function vs. method and generic vs. concrete:
func Int32N (n int32) int32 // function
func N [Int intType] (n Int ) Int // generic function
func (r *Rand) Int32N (n int32) int32 // method
func (r *Rand) N [Int intType] (n Int) Int // generic method
In this case, generic methods permit us to fix a small wart in the package API. This example isn't the motivating reason for adding generic methods, but I think it serves as an example of how adding them makes the language a bit simpler and more consistent. In Go, methods are just a type of function. Previously, you could write a type-parameterized function, but you couldn't write a type-parameterized method. That's an inconsistency that you need to remember. Now you can write type-parameterized functions or methods, using a consistent syntax for either.
Type-parameterized methods don't participate in interface satisfaction, so this change isn't without its own subtleties. Discussing the tradeoffs there would double the length of this post, and weighing them is why it took so long for us to decide to add generic methods.
Another possibility is that people will use generic methods to write unreadably complex code. My personal opinion is that nothing will stop people from writing unreadably complex code if they want to; the fix to complexity is to not do that.
mayama 13 hours ago [-]
Adding simd in std and even being used in map is nice. Would have to look for places to experiment with it in hot loops in code I have.
Instead of having each language bring progress in a different and/or novel, we get this, old java features that comes 20 years-in after the making.
They're probably useful, but clearly not sexy (as golang in general).
fweimer 9 hours ago [-]
> interfaces still can’t declare type-parameterized methods
What would an implementation look like? Wouldn't it be quite different from the existing one because it has to rely heavily on indirection because (limited) monomorphimization is not possible?
11 minutes ago [-]
cherryteastain 5 hours ago [-]
Probably won't/cannot be ımplemented. C++ and Rust disallow the analogues of this (templated virtual methods/dyn with a trait containing methods taking type parameters) as well.
lilbigdoot 15 hours ago [-]
This level of generics actually has me interested a bit in Go now.
bilinguliar 5 hours ago [-]
You can take my place, as the same changes make me want to leave.
Yes, thinking in higher order abstractions is hard for most people.
cpuguy83 4 hours ago [-]
I'm pretty sure the issue is not "higher order abstractions".
It is using multiple single letter references with no real grounding or relationship that the reader has to track.
For example, looping over a map with "k" and "v" vars is not that bad because the reader understands k=key and v=value, and that makes since for a map.
If you do this same thing with different single letter vars, e.g. "a" and "b", it instantly becomes more difficult to read.
When writing a generic function and using these single character type references it can make sense, especially because the function/method doesn't care what those references are, however to someone trying to understand what's going on it can be extremely difficult simply because of the names.
Sure, if all you are going to do is call that method or function those type references go away and the call site may be relatively clean, but you still have to read the thing to understand what it is and how to use it.
fooooor 2 hours ago [-]
On the contrary, linq in C# is a killer function that other languages still fail to replicate.
How many sloc are required for this?
var max = mycollection.Max();
Probably 5-10 if you’re missing higher order functions. And the risk of bugs will be 10x.
drivebyhooting 14 hours ago [-]
Can generics be used to improve error handling and eliminate the if err pattern?
ad_hockey 9 hours ago [-]
As of June last year[1] the Go team have pretty much drawn a line under this issue, with a very small amount of wiggle room to possibly reopen it at some point:
"For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling. We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation.”
Personally I'm OK with this, I didn't see any of the (many) proposals as a definite improvement. They all had trade-offs.
Yeah a few years ago there was a lot of buzz around it, but ultimately when presenting and polling all the options to the community, the general consensus was that existing error handling was actually fine. The other options added complexity and were harder to read.
fooooor 2 hours ago [-]
Rust solved this, but the Go language developers are having too much prestige to be able to embrace Rust’s solution. Sad.
jerf 13 hours ago [-]
No.
I kind of want to leave it there. But that will probably be looked on disfavorably.
I've seen at least a dozen attempts. It's not like it's hard to write it out. There's maybe a couple of variants but they're all just a handful of lines. The problem is, once you have an Option in hand, you end up trading:
val, err := whatever(...)
if err != nil {
// handle error
}
// use val
for
val := whatever(...)
if err, isErr := val.Error(); isErr {
// handle error
}
realVal := val.Value()
// use realVal
What you win in nominal safety, you're definitely losing in convenience.
There's also no win in trying to offer a monadic interface like
finalVal := whatever(...).OnVal(func (val Value) opt.Option[Result] {
// use val
})
because that's the minimal specification of an anonymous function in Go, so it's very inconvenient. Even if that was trimmed down, nested functions are still problematic in other ways. And you still have to unpack finalVal anyhow.
Really the solution is, install golangci-lint, turn on errcheck [1], use a pre-commit hook to make it a commit failure if golangci-lint fires, and that pretty much covers the problem in practice.
One of the problems with Option/Result/etc. advocacy... not the pattern itself, the advocacy... is that it is generally are presented, implicitly or explicitly, as if the alternative is C, with its errno and the need to not just check an error value, but remember to go actively seeking out errors constantly, making it easy to forget. But by modern standards, that's completely pathological.
If we rate error handling techniques on a scale from 1 to 10 (best), C here is a 1, and standard Option is maybe an 8 or a 9. The way Go does it is maybe a 6; it is completely true that you can neglect to handle an error (see errcheck comment in previous paragraph), but it is in your face that an error is possible, and that's really most of the problem. Putting Option/Result/etc. is not always a "go from 1 to 9" result. "Go from 6 to 8" is a much less impressive proposition, and the other inconveniences that come with it in Go tend to overwhelm the gain. I use errcheck all the time, and even in the Before Times when I was writing it all by hand it really didn't fire all that often. Especially if I exclude test code. In an AI era this hardly rates at all. AI never neglects the error.
Whether it does the right thing with it, now... that's another story entirely.
Read those error handling clauses if you're writing Go with AI. I really don't like what I've seen AIs do with them by default. What I've seen out of AI has been very thoughtless. Nominally correct in some weak sense, but thoughtless.
I think my opinion will be even more maligned:
I like Java-style checked exceptions. It forces awareness of the error and a clean way to propagate it.
adrianmsmith 9 hours ago [-]
People advocate for Go's error handling because it forces you to deal with errors.
But in the cases I do want to catch a specific error, the signature only tells me that a function returns an error, not which type. So I do feel that returning (int, error) is strictly worse than Java's checked exceptions if you care about errors.
spockz 9 hours ago [-]
This is elegantly solved with sum types. (And arguably needs sub typing.)
If instead of just returning (int, error) the function would return (int, parseError | outOfBoundsError) you would know that the parser function can fail on reading a number at all and on the number being to big/small to fit the type and handle then accordingly.
Saliently, Java in a sense has supported sum types in the throws declaration and the subsequent catch statements forever. Unfortunately it has not landed in other places where you can use types so you cannot use it for returning errors. Scala 3 supports this but has tiny adoption it seems.
kitd 9 hours ago [-]
That's helped by errors.Unwrap() and errors.Is(), though you do need to build and structure your errors appropriately.
adrianmsmith 8 hours ago [-]
It's helped, but that's runtime behaviour. The compiler can't help you with questions like "you didn't handle certain errors" or "the error you're trying to check can never occur".
Nor does that help with documentation, I'm guessing if I read a file then it might raise the error that the file does not exist. But what exactly is the technical type of that error? You have to search through the function's implementation, and functions that function calls, etc., to find out.
And it doesn't help with refactoring. If you create new.FileNotFoundError and change your function to return it, existing code which checks for old.FileNotFoundError will start to silently fail.
mook 9 hours ago [-]
I agree; in practice, probably only the public error types, though. If it can return fmt.Errorf("…%w…") I probably only need the type of the wrapped error, not whatever type the implementation uses.
girvo 5 hours ago [-]
I liked Nim’s take on checked exceptions personally, was quite lovely
Nursie 12 hours ago [-]
The thing that most annoys me in Java in that area is when a dependency throws an unchecked error that wasn’t even documented.
Thanks so much for that! Now I have no choice but to be reactive when something fails…
bajsejohannes 5 hours ago [-]
If you get the ? from rust in addition to the option, then it’s suddenly a lot more convenient. It will do `if err != nil { return err; }` in a single symbol.
There’s also convenience at the returning side. You can always just return the error, and not have to care about dummy values for the other return values (which is especially annoying when changing the returned types).
That said, it might still not be worth the added complexity.
nitrix 10 hours ago [-]
Error handling is as important as the happy path of an application. It’s not something you sweep under the rug.
The err != nil quickly turns into metrics, logs, fallback strategies, retry mechanisms, flight recording, rate limiting, updating caches, so on.
Anyone who’s trying to shorten this hasn’t maintained any actual real software.
theplumber 13 hours ago [-]
This is quite of a big release and I like the new methods on generics.
Hendrikto 6 hours ago [-]
Methods on generic types were already a thing. What is new is that methods can now define their own generic parameters, independent of the type they are defined on.
rednafi 6 hours ago [-]
I am all for using LLMs to generate value but a little more editorial review can't hurt.
> The quieter but bigger change: the classic encoding/json (v1) package is now backed by the v2 implementation under the hood.
This is fantastic content nevertheless.
nothrows 13 hours ago [-]
generics were a slippery slope. give it a decade and Go will be indistinguishable from c++
EdiX 12 hours ago [-]
Generics themselves maybe not. Generic methods probably yes. What people want generic methods for is to do deeply nested call chains that were never typical of Go. And if you have deeply nested calls you'll need some way to deal with errors in deeply nested calls, and then a short function syntax to pass to behavior inside those deeply nested calls. Give it a few years and everyone will be writing the same functional slop in Go that they are writing in every other language.
adrianmsmith 9 hours ago [-]
> What people want generic methods for is to do deeply nested call chains
I don't see that as the only use of generic methods.
The example in the article is a "Map" method that transforms e.g. a List[A] to a List[B], by taking a function that takes an A and returns a B. To be able to transform a list like that is a useful operation.
It was possible to do the same with a global function like MapList but the syntax is nicer if you use methods. You don't need the type in the name (function MapList vs method Map) and it is an operation on the List after all so list.Map(..) is nicer than MapList(list, ..).
foldr 10 hours ago [-]
The generic methods that have been added are essentially just syntax sugar. You can now use method syntax in cases where you could equivalently define a function. They’re not the fundamental extension to the type system that some people have been asking for (and probably will never get, because there’d be no reasonable way to implement it).
nirui 12 hours ago [-]
Not here against Generic methods, but I feel the Go team is in a mid-age crisis where they lack of new things to do to prove themselves. See their iterators mini-drama not long ago?
I feel the sumtype/emum/routine demanders should yell a little harder so Go team can find their purpose again.
cookiengineer 13 hours ago [-]
> generics were a slippery slope. give it a decade and Go will be indistinguishable from c++
Lib boost will have conquered every language by then!!! :D
Jokes aside, generics are unusable in a lot of languages due to their syntax choices. In Go we kinda have the problem that there's no real templating and no real macros, so they're even harder to use.
But I agree somewhat, generics feels to me like an anti pattern in Go.
Also, the way the Go core/stdlib is written, it makes generics so unnecessarily painful to debug. Why they decided to have definitions like "~C" or "~[]S" is beyond me. No human knows what the resulting compile time error means. They should have named these things "Comparable" or "Slicable" or whatever is more expressive. Just stop with this stupid single letter shit.
stingraycharles 15 hours ago [-]
Am I the only one who’s absolutely shocked that Go finally is embracing generics?
Does anyone have a bit of an inside view into what changed in the perspectives of the language maintainers?
I’m not buying the “it took us 20 years to understand how to do it correctly” argument, as this is something you explicitly take into consideration when designing the language or not. And it was specifically not a part of language design, and is much harder to retrofit (backwards compatibility).
So what changed?
bradfitz 14 hours ago [-]
(I was on the Go team for ages)
Seriously, that's all it was. Just Ian alone proposed and rejected a half dozen of his own different approaches to generics. Finally a language + implementation plan came together that people all liked.
Nobody was ever opposed to generics that I saw.
amtamt 13 hours ago [-]
If bug free binary search implementation can take 16 years, I am ready to buy generics implementation could take 20 years.
> In his landmark book The Art of Computer Programming, legendary computer scientist Donald Knuth noted that although the first binary search algorithm was published by John Mauchly in 1946, the first bug-free version was not published until 1962—taking a staggering 16 years to get right.
ckcheng 13 hours ago [-]
Took a few more years to get really bug free.
> Fast forward to 2006. I was shocked to learn that the binary search program that Bentley proved correct and subsequently tested in Chapter 5 of Programming Pearls contains a bug. ... Lest you think I'm picking on Bentley, let me tell you how I discovered the bug: The version of binary search that I wrote for the JDK contained the same bug. It was reported to Sun recently when it broke someone's program, after lying in wait for nine years or so.
IIRC it was overflow when you do (a + b) / 2 for the midpoint. It took so long to find because you need a >billion item array to overflow the 32-bit integer, and that much RAM wasn't common until the 00s.
The right way is a + (b - a)/2.
fooster 14 hours ago [-]
It’s also not true that because it wasn’t part of the initial design that it was harder to retrofit. I just don’t understand all this go bashing that happens on this site especially when so much is badly informed speculation. I guess it’s easier to tear something down.
stingraycharles 10 hours ago [-]
my post was never intended as Go bashing, I use Go a lot and appreciate its simplicity.
am I tearing something down in my comment?
foldr 10 hours ago [-]
HN threads on Go are boring because they always get derailed by someone grousing about the fact that it took a long time to add generics. The history of this has been gone over a thousand times already and it’s really quite undramatic. The Go team couldn’t figure out a good design for generics for a long time. Eventually, they got some help from Phil Wadler and other type system experts and figured it out. The end. Anyone who feels that the Go team should have done it faster owes us at the very least their own design together with a soundness proof for a plausible fragment of Go. Conspicuously, no-one provided such a thing before the Go team did.
The details of Go generics, their advantages and disadvantages compared to other languages, etc., are absolutely interesting to discuss. But there is nothing hiding behind the “official” story.
HumblyTossed 4 hours ago [-]
Surely it was pressure from devs to make Go look like every other language they begged to change then abandoned for the new hotness.
abtinf 13 hours ago [-]
Unfortunately nothing changed. They wanted generics all along.
The Go ecosystem was a delicate, special thing. It was a wholesale rejection of the malignant consultancy takeover of programming that had festered and spread for the previous 15 years. Introducing generics was a grievous error, and they just keep making it worse.
It used to be you could look at any Go code from any author and pretty much instantly understand it completely. That’s no longer the case.
It used to be you would work on a problem, just writing the code from top to bottom. No time wasted fiddling with abstractions you’ll never use. You’d grumble about it, but succumbing to the temptation was impossible. That’s no longer the case.
tacitusarc 12 hours ago [-]
I have written Go for the past decade and completely, fundamentally disagree with this take. Go has always had a tendency towards limited exressivity, which created a strong dependence on interface{}, type assertions, and runtime bug’s that should have been compiler errors.
When I read these grumbling takes about how Go use to be so simple etc I imagine devs who would revel in all the features they were unable to implement because it would be too difficult in the language. Or devs who love typing and re-typing the same code over and over again, littering their code with switch cases and conditional logic while passing themselves on the back for avoiding “abstraction”.
wredcoll 12 hours ago [-]
[dead]
whateveracct 13 hours ago [-]
and they're still worse than the 1970s state of the art lol
eager_learner 11 hours ago [-]
you mean like in Oberon?
whateveracct 4 hours ago [-]
ML's parametric polymorphism
foldr 4 hours ago [-]
Go lets you constrain type parameters with interfaces. To do anything analagous in SML you have to use functors, which is substantially less convenient. I think people who refer to ML's parametric polymorphism in this context must really be thinking of parametric polymorphism in OCaml or Haskell.
Fine in principle, but AFAIK it never really caught on because the ergonomics suck. Interfaces/traits/type classes do seem to be a popular feature across languages, for what it's worth.
throw2ih020 14 hours ago [-]
> what changed in the perspectives of the language maintainers?
The original maintainers moved on to other projects and the new community maintainers came to a consensus through the proposal and governance process.
bradfitz 14 hours ago [-]
No, that's not accurate. The same core people were involved.
Tried running a couple of examples in the tour, but ran into a few errors.
wredcoll 12 hours ago [-]
Tried reading your comment but ran into a lack of usable information.
wccrawford 7 hours ago [-]
If you try a new thing and run into 1 (or maybe 2 errors), maybe it's worth the time to report them.
If you run into a few errors, it's into "this isn't ready" or "they didn't try hard enough" territory, and it's not worth reporting the problems.
12 hours ago [-]
mangudai 2 hours ago [-]
[flagged]
smalljelly2018 8 hours ago [-]
[flagged]
fang2hou 13 hours ago [-]
[dead]
okzgn 14 hours ago [-]
[dead]
vladsiu 10 hours ago [-]
[dead]
pixxxel 11 hours ago [-]
Let's GO
adrianmsmith 7 hours ago [-]
One thing I think generics in Go is missing is the <?> concept in Java.
If you're taking a List[T] and all you want to do is to call list.size() then you don't care what type of list it is. In Java you can write a function which takes a List<?> but in Go you have to write List[T] so then the question becomes what is T?
You have to make the function (or type you're a method on) generic. If you make the type generic then every user of your type also needs to specify T, etc.
I don't think it would be impossible to add that to Go. Allow List[?], which matches a List with any type parameter. Calling functions which don't involve the type parameter like list.size() would be fine, calling a method returning the type parameter like list.get(n) would return "any", and methods taking the type parameter like list.set(n, obj) would probably not be callable.
kbolino 1 hours ago [-]
You need this in Java because interfaces are explicitly implemented. You don't need this in Go because interfaces are structurally implemented. The way you spell "anything that has a method named Size which returns int" is interface{Size() int}.
Hendrikto 6 hours ago [-]
Go Generics work differently than those in Java. They are specialized, meaning that they are not generic at runtime anymore. Instead, the compiler creates a different implementation for each type.
At runtime, there are only List[int], List[string], etc. List[T] is not a thing anymore.
Could someone take the example, reduce it to a non-generic version for two types I DO understand, then show that with the new feature I can collapse them into the Box/Map example in the doc?
I have 10+ years of Go experience and I can't make heads or tails of "(b Box[T]) Map[U any](f func(T) U) Box[U]"
I think it's trying to show a mapping operation for a generic container where the container values are of one type and the mapping function is allowed to return a container with values of a different type.
Without generics, something along the lines of the following (with runnable example at https://go.dev/play/p/KHBI1uAhbO0):
From a quick search, this seems to be better explanation of this new 1.27 feature:https://www.gopherguides.com/articles/golang-generic-methods
(That uses an example that seems similar in spirit to the Interactive Tour's example, but with a more useful type of a Stack[T] and corresponding explanation seem clearer.)
(Previously, you could achieve the same net effect with a top-level generic function, but then the code would not be grouped as nicely as hanging it off of the type, and arguably it now can have slightly better ergonomics in some cases. You can see more of the rational from Robert Griesemer at https://github.com/golang/go/issues/77273.)
It wasn't a great example because "Box" isn't really a useful type. But the point is that you no longer need to define a separate "MapToXXX" method for every type you might want to map to; now you can have just one type-generic "Map" method.
I understand the desire to keep things concrete and avoid high level abstractions, but it's a decision not to automate stuff that can easily be automated. It runs counter to the basic instincts and purpose of our field/industry. That's why it never sticks.
Most of the time generics might be useful, I’ve ended up needing reflection too anyway. And at that point, I’m really no better off for generics.
The problem is generics only solve a very small part of the equation: compile time checks for composite types. But to use composite types in anything non-trivial in Go, you then need reflection. Which is slow. And if you then need reflection, you’re already passing interface types anyway plus you’re back to having to handle type-handling errors in the runtime.
So if you’re writing a library that’s expected to have any kind of performance, you’re back to code duplication and having a DoSomethingType() function signatures again.
Or you stick with reflection and take that performance hit PLUS the risk of compile time constraints being runtime errors; which is the a lose-lose scenario. And let’s also not forget that reflection can be just as verbose as code duplication, and harder to get right too.
Don’t get me wrong, I’m glad we have generics. But people on HN massively overstate the value of them in a AOT non-dynamic, strictly typed language like Go.
I guess you could argue that Go has other shortcomings that directly result in generics having limited value. But then you’re basically just arguing that you prefer coding in a different language paradigm, and at that point, you’re much better off using that other paradigm instead of complaining that Go isn’t JavaScript or Haskell.
In Haskell as well, you can let the compiler infer a lot of things but that doesn’t appear to be the case with this example.
I’d want the compiler to infer things, but that - I think - is at odds with Go desiring a fast compiler, which I also understand.
Unless you're writing assembler in vim you're not STEM.
I think that code would be a lot easier to read if the types were called IN and OUT or In and Out or TIn and TOut or something like that.
I guess the single letter thing is laziness for a part. It's not simple to find words that represent the abstract idea behind the generic type without narrowing the possibilities. For array function, the Key Value from the sibling comment work but for more complex use case, it get complicated.
for (int i=0; i<10; i++) { printf(”%d\n”, i); }
(Or the very similar Go equivalent)
If you having a hard time parsing that, due to the short variable name, i.e. if it’s a huge cognitive load for you, I suggest you switch career, b/c the IT industry is obviously not a good fit.
With that said, Go is explicit with suggesting short variable names for small scopes, and long variable names for bigger scopes. This a good practice in all languages.
I think that’s best as you’ll soon learn the “single-character capital letter ⇒ generic parameter” convention
There's IList<T> but Task<TResult>
There's Action<T1, T2, T3, T4, T5, T6> but also Dictionary<TKey, TValue> and Map<TIn, TOut>
This stuff kind of "makes sense" once you're used to it, because it's difficult to say what IList<T> ought to have been called otherwise, IList<TContainee> is a mouthful, and Action<T1,...> simply suffers from the inability to specify an unknown number of generic parameters.
https://learn.microsoft.com/en-us/dotnet/api/system.collecti...
https://learn.microsoft.com/en-us/dotnet/api/system.action-2...
https://learn.microsoft.com/en-us/dotnet/api/system.collecti...
I dont find it confusing, as its pretty clear that it only an placeholder.
In generics the name usually does not matter or is REALLY hard to name so that it makes sense.
More specifically in Go where you have interfaces, concrete types and generics.
If you don't want it don't use it. It's that simple.
Also, screw those Romans ;)
But anyway I find this in Go much more bearable.
Unless you are a compiler/stdlib vendor or contributing to Boost, there are features that you just don't use it daily.
The T, U, and f names are the cognitive load here, because they are meaningless variables. For a specific solution, those would have meaningful names that would make it easier to understand.
There are 37000 programming languages, stop forcing every single one that gets popular to look like this.
[1] https://go-review.googlesource.com/c/go/+/749062
[2] https://go-review.googlesource.com/c/go/+/751020
Now in 1.27:
> http.Response.Body drains itself on Close. For HTTP/1, closing the body now reads and discards any unread content (up to a conservative limit) so the connection can be reused. For most programs this is a transparent win [...]
Great, so i no longer have to io.Copy(io.Discard, resp.Body) in the err case, one less thing to worry about; but
> if you were leaning on an early Close to abort a large download, set Transport.DisableKeepAlives to opt out.
That's a subtle behaviour change. Any previous Go program which used Close in this way - say for an infinite event stream - now hangs, soaking up bandwidth.
In the past, the Go team have searched the entire Github corpus for misuse before making changes like this. I don't have a reference but I assume an appropriate level of consideration went into this decision.
EDIT: ""up to a conservative limit"" so this is not so bad after all.
Anyway, this is why it pays off to read release notes closely and have a decent test suite.
Go 1.26 in practice never re-used that connection, it always established a new one because you can't reuse a connection which has a pending response ready to be read.
Go 1.27 will now consume the body for you, causing your application to re-use connections much more aggressively, bringing in potential edge cases (e.g. dependency is broken, connection is now permanently unusable, your app no longer recovers automatically).
To be clear, I'm very glad for the change and I had equivalent code in our in-house framework to do just that, but yeah it does change the behavior in a way that it could expose undetected issues.
I really wish they didn't use such stupid LLM-isms.
"The creatures outside looked from pig to man, and from man to pig, and from pig to man again; but already it was impossible to say which was which." ― George Orwell, Animal Farm
We were doing this before LLM's. All sorts of trendy business speak would spread - the term "synergy" springs to mind as one people beat to death.
This is the concept of "memetics" (as in meme) in action. Hank Green recently talked about using AI and he went "off script" and threw "I appreciate the pushback" into his speech on the fly...
LLM's generating large volumes of content means that we're going to see all of its "isms" creep into other peoples speech much faster.
java feels kinda unhinged the more that i look at it
:x i wonder if anyones done something like this, would be super unhinged go seems to get a lot of flack around these parts. i kinda lurv it though, just getting compiled binaries out of not much code and not needing a runtime to do shtuff. once i got a wrangle on goroutines i dunno i feel like its pretty solid for webapp backend which is mostly what i use it forBack when Go's generics came out, I was working with about 20% Go and 80% Python. I looked at the syntax, went "not today, Satan" and never bothered to learn it. For the past half year I've been in a mode where most of my coding time is spent with Go and I've been completely indoctrinated. I unironically like thinking about how generics and interfaces interact now.
I also need to slow down when I need to use them for non-trivial stuff. Not just relative to Go code but relative to how much I needed to think about them back when I was using OCaml. I think part of it is that I save them for hard issues and use interfaces for easy stuff.
The math/rand/v2 package has a number of functions which return random numbers of a certain type:
It has functions which return a number within a range: It also has a generic function, rand.N, where the return type is set by a type parameter. The definition of rand.Int32N (for comparison) and rand.N are: Adding some spaces to make the common elements align (apologies if my formatting gets mangled), that's: As you can see, the generic function N has the same signature as the non-generic Int32N, except the type it operates on is set by a type parameter (named "Int"). The type parameter has a constraint, intType, which is a private type defined in the math/rand package. (There's nothing magic about this constraint, it's just a list of all the integer types in the language, and you can write it yourself if you want to. It's a separate type to keep the function signature of N from becoming too large, and it's internal to math/rand because it doesn't need to be part of the public package API.)The nice thing about rand.N is that it lets you write something like this:
Without generics, you'd instead write this as the following, which is a lot more noise: The generic rand.N has a more confusing type signature and a lot more language complexity behind it, but the code using it is simpler and easier to read. We think that's a good tradeoff, but of course not everyone will agree.All the functions I've mentioned so far use a default random number source. Each of them also exists as a method of the rand.Rand type, which generates numbers from a user-provided randomness source. For example:
There is one exception, though: Until Go 1.27, there was no Rand.N method, because we did not support generic methods. (A generic type could have methods, but those methods could not be further type parameterized.)In Go 1.27, there is now a Rand.N method:
This method's signature is: Comparing function vs. method and generic vs. concrete: In this case, generic methods permit us to fix a small wart in the package API. This example isn't the motivating reason for adding generic methods, but I think it serves as an example of how adding them makes the language a bit simpler and more consistent. In Go, methods are just a type of function. Previously, you could write a type-parameterized function, but you couldn't write a type-parameterized method. That's an inconsistency that you need to remember. Now you can write type-parameterized functions or methods, using a consistent syntax for either.Type-parameterized methods don't participate in interface satisfaction, so this change isn't without its own subtleties. Discussing the tradeoffs there would double the length of this post, and weighing them is why it took so long for us to decide to add generic methods.
Another possibility is that people will use generic methods to write unreadably complex code. My personal opinion is that nothing will stop people from writing unreadably complex code if they want to; the fix to complexity is to not do that.
They're probably useful, but clearly not sexy (as golang in general).
What would an implementation look like? Wouldn't it be quite different from the existing one because it has to rely heavily on indirection because (limited) monomorphimization is not possible?
That's completely unreadable.
For example, looping over a map with "k" and "v" vars is not that bad because the reader understands k=key and v=value, and that makes since for a map. If you do this same thing with different single letter vars, e.g. "a" and "b", it instantly becomes more difficult to read.
When writing a generic function and using these single character type references it can make sense, especially because the function/method doesn't care what those references are, however to someone trying to understand what's going on it can be extremely difficult simply because of the names.
Sure, if all you are going to do is call that method or function those type references go away and the call site may be relatively clean, but you still have to read the thing to understand what it is and how to use it.
How many sloc are required for this?
var max = mycollection.Max();
Probably 5-10 if you’re missing higher order functions. And the risk of bugs will be 10x.
"For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling. We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation.”
Personally I'm OK with this, I didn't see any of the (many) proposals as a definite improvement. They all had trade-offs.
[1] https://go.dev/blog/error-syntax
I kind of want to leave it there. But that will probably be looked on disfavorably.
I've seen at least a dozen attempts. It's not like it's hard to write it out. There's maybe a couple of variants but they're all just a handful of lines. The problem is, once you have an Option in hand, you end up trading:
for What you win in nominal safety, you're definitely losing in convenience.There's also no win in trying to offer a monadic interface like
because that's the minimal specification of an anonymous function in Go, so it's very inconvenient. Even if that was trimmed down, nested functions are still problematic in other ways. And you still have to unpack finalVal anyhow.Really the solution is, install golangci-lint, turn on errcheck [1], use a pre-commit hook to make it a commit failure if golangci-lint fires, and that pretty much covers the problem in practice.
One of the problems with Option/Result/etc. advocacy... not the pattern itself, the advocacy... is that it is generally are presented, implicitly or explicitly, as if the alternative is C, with its errno and the need to not just check an error value, but remember to go actively seeking out errors constantly, making it easy to forget. But by modern standards, that's completely pathological.
If we rate error handling techniques on a scale from 1 to 10 (best), C here is a 1, and standard Option is maybe an 8 or a 9. The way Go does it is maybe a 6; it is completely true that you can neglect to handle an error (see errcheck comment in previous paragraph), but it is in your face that an error is possible, and that's really most of the problem. Putting Option/Result/etc. is not always a "go from 1 to 9" result. "Go from 6 to 8" is a much less impressive proposition, and the other inconveniences that come with it in Go tend to overwhelm the gain. I use errcheck all the time, and even in the Before Times when I was writing it all by hand it really didn't fire all that often. Especially if I exclude test code. In an AI era this hardly rates at all. AI never neglects the error.
Whether it does the right thing with it, now... that's another story entirely.
Read those error handling clauses if you're writing Go with AI. I really don't like what I've seen AIs do with them by default. What I've seen out of AI has been very thoughtless. Nominally correct in some weak sense, but thoughtless.
[1]: https://golangci-lint.run/docs/linters/configuration/#errche...
But in the cases I do want to catch a specific error, the signature only tells me that a function returns an error, not which type. So I do feel that returning (int, error) is strictly worse than Java's checked exceptions if you care about errors.
If instead of just returning (int, error) the function would return (int, parseError | outOfBoundsError) you would know that the parser function can fail on reading a number at all and on the number being to big/small to fit the type and handle then accordingly.
Saliently, Java in a sense has supported sum types in the throws declaration and the subsequent catch statements forever. Unfortunately it has not landed in other places where you can use types so you cannot use it for returning errors. Scala 3 supports this but has tiny adoption it seems.
Nor does that help with documentation, I'm guessing if I read a file then it might raise the error that the file does not exist. But what exactly is the technical type of that error? You have to search through the function's implementation, and functions that function calls, etc., to find out.
And it doesn't help with refactoring. If you create new.FileNotFoundError and change your function to return it, existing code which checks for old.FileNotFoundError will start to silently fail.
Thanks so much for that! Now I have no choice but to be reactive when something fails…
There’s also convenience at the returning side. You can always just return the error, and not have to care about dummy values for the other return values (which is especially annoying when changing the returned types).
That said, it might still not be worth the added complexity.
The err != nil quickly turns into metrics, logs, fallback strategies, retry mechanisms, flight recording, rate limiting, updating caches, so on.
Anyone who’s trying to shorten this hasn’t maintained any actual real software.
> The quieter but bigger change: the classic encoding/json (v1) package is now backed by the v2 implementation under the hood.
This is fantastic content nevertheless.
I don't see that as the only use of generic methods.
The example in the article is a "Map" method that transforms e.g. a List[A] to a List[B], by taking a function that takes an A and returns a B. To be able to transform a list like that is a useful operation.
It was possible to do the same with a global function like MapList but the syntax is nicer if you use methods. You don't need the type in the name (function MapList vs method Map) and it is an operation on the List after all so list.Map(..) is nicer than MapList(list, ..).
I feel the sumtype/emum/routine demanders should yell a little harder so Go team can find their purpose again.
Lib boost will have conquered every language by then!!! :D
Jokes aside, generics are unusable in a lot of languages due to their syntax choices. In Go we kinda have the problem that there's no real templating and no real macros, so they're even harder to use.
But I agree somewhat, generics feels to me like an anti pattern in Go.
Also, the way the Go core/stdlib is written, it makes generics so unnecessarily painful to debug. Why they decided to have definitions like "~C" or "~[]S" is beyond me. No human knows what the resulting compile time error means. They should have named these things "Comparable" or "Slicable" or whatever is more expressive. Just stop with this stupid single letter shit.
Does anyone have a bit of an inside view into what changed in the perspectives of the language maintainers?
I’m not buying the “it took us 20 years to understand how to do it correctly” argument, as this is something you explicitly take into consideration when designing the language or not. And it was specifically not a part of language design, and is much harder to retrofit (backwards compatibility).
So what changed?
Seriously, that's all it was. Just Ian alone proposed and rejected a half dozen of his own different approaches to generics. Finally a language + implementation plan came together that people all liked.
Nobody was ever opposed to generics that I saw.
> In his landmark book The Art of Computer Programming, legendary computer scientist Donald Knuth noted that although the first binary search algorithm was published by John Mauchly in 1946, the first bug-free version was not published until 1962—taking a staggering 16 years to get right.
> Fast forward to 2006. I was shocked to learn that the binary search program that Bentley proved correct and subsequently tested in Chapter 5 of Programming Pearls contains a bug. ... Lest you think I'm picking on Bentley, let me tell you how I discovered the bug: The version of binary search that I wrote for the JDK contained the same bug. It was reported to Sun recently when it broke someone's program, after lying in wait for nine years or so.
https://research.google/blog/extra-extra-read-all-about-it-n...
Loads fine for me:
> The bug is in this line:
6: int mid =(low + high) / 2;
The right way is a + (b - a)/2.
am I tearing something down in my comment?
The details of Go generics, their advantages and disadvantages compared to other languages, etc., are absolutely interesting to discuss. But there is nothing hiding behind the “official” story.
The Go ecosystem was a delicate, special thing. It was a wholesale rejection of the malignant consultancy takeover of programming that had festered and spread for the previous 15 years. Introducing generics was a grievous error, and they just keep making it worse.
It used to be you could look at any Go code from any author and pretty much instantly understand it completely. That’s no longer the case.
It used to be you would work on a problem, just writing the code from top to bottom. No time wasted fiddling with abstractions you’ll never use. You’d grumble about it, but succumbing to the temptation was impossible. That’s no longer the case.
When I read these grumbling takes about how Go use to be so simple etc I imagine devs who would revel in all the features they were unable to implement because it would be too difficult in the language. Or devs who love typing and re-typing the same code over and over again, littering their code with switch cases and conditional logic while passing themselves on the back for avoiding “abstraction”.
Fine in principle, but AFAIK it never really caught on because the ergonomics suck. Interfaces/traits/type classes do seem to be a popular feature across languages, for what it's worth.
The original maintainers moved on to other projects and the new community maintainers came to a consensus through the proposal and governance process.
If you run into a few errors, it's into "this isn't ready" or "they didn't try hard enough" territory, and it's not worth reporting the problems.
If you're taking a List[T] and all you want to do is to call list.size() then you don't care what type of list it is. In Java you can write a function which takes a List<?> but in Go you have to write List[T] so then the question becomes what is T? You have to make the function (or type you're a method on) generic. If you make the type generic then every user of your type also needs to specify T, etc.
I don't think it would be impossible to add that to Go. Allow List[?], which matches a List with any type parameter. Calling functions which don't involve the type parameter like list.size() would be fine, calling a method returning the type parameter like list.get(n) would return "any", and methods taking the type parameter like list.set(n, obj) would probably not be callable.
At runtime, there are only List[int], List[string], etc. List[T] is not a thing anymore.