Go 1.27 Interactive Tour
Posted by Hixon10 1 day ago
Comments
Comment by baalimago 1 day ago
Comment by teh64 1 day ago
(b Box[InType]) Map[OutType any](transformFunction func(InType) OutType) Box[OutType]
Same in Python: def map[U](self, f: Callable[[T], U]) -> Box[U]
vs def map[OutType](self, transform_function: Callable[[InType], OutType]) -> Box[OutType]
and Java: public <OutType> Box<OutType> map(Function<InType, OutType> transformFunction)
vs. public <U> Box<U> map(Function<T, U> f)Comment by fauigerzigerk 1 day ago
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.
Comment by hnlmorg 1 day ago
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.
Comment by fauigerzigerk 1 day ago
Comment by hnlmorg 1 day ago
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.
Comment by kccqzy 21 hours ago
The Go language itself is never its strength but it has a good runtime, wonderful standard library and tooling. People never picked Go for being an amazing language, but rather for these other things.
Comment by hnlmorg 9 hours ago
Go’s shortcoming is also its strength. Language design is a constant battle of tradeoffs. And I happen to find many of the decisions C++ and Rust made weren’t analogous with my preferences.
> It’s still Go’s fault and people rightfully argue that they should prefer a different language.
It’s no more Gos fault than it is the people using Go. It’s called an “opinion” and “preference”. Please don’t assuming your preference is some global truth, because it is not.
Comment by foldr 3 hours ago
Go generics does have this feature. You can require a generic type to implement an interface.
Comment by theptip 1 day ago
Comment by pezo1919 1 day ago
Comment by preg_match 22 hours ago
The thing is that one use case is so essential, so foundational, that we really can’t just skip it. You need generic containers, for ergonomics and performance. I mean, compare C qsort to C++ std::sort.
The languages that “get around” generics, like PHP, include god containers in the runtime. The language I’m designing is also that way, I’d like to avoid generics preferably forever.
But there’s a tradeoff there. God containers are very flexible, and we’ve seen the ramifications of untyped PHP arrays.
Comment by Pay08 1 day ago
Comment by fauigerzigerk 1 day ago
Comment by stingraycharles 1 day ago
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.
Comment by Pay08 1 day ago
Comment by adrianmsmith 1 day ago
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.
Comment by majewsky 8 hours ago
Comment by jiehong 1 day ago
Comment by toinebeg 1 day ago
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.
Comment by spockz 1 day ago
Comment by wwalexander 1 day ago
Comment by Someone 1 day ago
I think that’s best as you’ll soon learn the “single-character capital letter ⇒ generic parameter” convention
Comment by fooooor 1 day ago
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.
Comment by phplovesong 1 day ago
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.
Comment by asQuirreL 1 day ago
Comment by teh64 1 day ago
val map : ('a Box) -> ('a -> 'b) -> 'b BoxComment by Laurel1234 1 day ago
Comment by eterm 1 day ago
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...
Comment by setopt 1 day ago
Comment by logicchains 1 day ago
Comment by zerr 1 day ago
Comment by red_admiral 1 day ago
Comment by yladiz 1 day ago
Comment by kfuse 23 hours ago
interface Box<T> { value: T }
function map<T, U>(input: Box<T>, func: (value: T) => U): Box<U> {
return { value: func(input.value) }
}Comment by sirsinsalot 21 hours ago
Your comment boils down to "I'm smart", which in the end, isn't terribly smart.
Having simplicity and expressiveness as a goal, and a general direction of achieving things through lazy means is at the heart of mathematics and engineering.
Celebrate laziness and a want for simplicity. True simplicity is hard, but worth going after even where it threatens the notion that you're the smartest person in the room.
Comment by SubjectToChange 10 hours ago
They are saying that a programmer should be able to cope with the cognitive dissonance of not immediately understanding something.
>Celebrate laziness and a want for simplicity.
Concepts like generics might be intellectually more challenging, but they are clearly the “lazy” approach for actually writing code. Writing and maintaining multiple versions of the same function, or using code generation, is intellectual lazy but manually intensive.
Comment by wannabe44 1 day ago
Comment by SubjectToChange 9 hours ago
Golang has good “implementation level” tooling, but that is still nothing when compared to tooling available to Java and C#. Even if GoLand is closing the gap on the IDE front (idk, I haven’t tried it), golang simply doesn’t offer the features found in OpenJDK or Dotnet (GC parameters/implementations, profiling/introspection data, interoperability/ffi, etc.). It just feels like golang’s tooling looks good when it’s competing against python, ruby, or JS/TS.
Comment by thebytefairy 1 day ago
Comment by snsjjsjjs 23 hours ago
Comment by 4ndrewl 1 day ago
Unless you're writing assembler in vim you're not STEM.
Comment by fragmede 1 day ago
Comment by dvdkon 1 day ago
func (b: Box[T]).Map[U: any](f: func(T) -> U) -> Box[U]Comment by mseepgood 1 day ago
Comment by wannabe44 1 day ago
SortBy[T, K comparable](slice: []T, key: func (T) K)Comment by red_admiral 1 day ago
Comment by treyd 1 day ago
If you don't want it don't use it. It's that simple.
Comment by fooooor 1 day ago
Comment by golem14 1 day ago
Also, screw those Romans ;)
Comment by inigyou 1 day ago
Comment by twsted 1 day ago
But anyway I find this in Go much more bearable.
Comment by zerr 1 day ago
Unless you are a compiler/stdlib vendor or contributing to Boost, there are features that you just don't use it daily.
Comment by majewsky 8 hours ago
Comment by zerr 7 hours ago
Comment by kitd 1 day ago
Comment by fooooor 1 day ago
Comment by skywhopper 1 day ago
(b IntBox) MapToStringBox(f func(int) string) StringBox
(b IntBox) MapToBoolBox(f func(int) bool) BoolBox
(b StringBox) MapToIntBox(f func(string) int) IntBox
Etc etc etc?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.
Comment by scotty79 1 day ago
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.Comment by wannabe44 1 day ago
Comment by inigyou 1 day ago
Comment by HumblyTossed 1 day ago
There are 37000 programming languages, stop forcing every single one that gets popular to look like this.
Comment by majewsky 8 hours ago
Comment by YesThatTom2 1 day ago
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]"
Comment by LukeShu 1 day 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.
Comment by majewsky 8 hours ago
It is very close to one. My Option[T] type [1] cannot have a Map method in stable Go because of the type system restriction in question. Instead, I have a separate package with freestanding functions with the same purpose, including Map [2].
[1] https://pkg.go.dev/go.xyrillian.de/gg/option#Option [2] https://pkg.go.dev/go.xyrillian.de/gg/options#Map
Comment by typical182 1 day ago
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):
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: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.)
Comment by typical182 1 day ago
// 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 rationale from Robert Griesemer at https://github.com/golang/go/issues/77273.)
Comment by torginus 23 hours ago
This is like building a very crude general-ish DSL inside the language. Because the tools are intentionally limited (as to limit the scope of the feature), the result looks ugly. Also, like with C++ templates, people find exploits to do what the designers didn't want them to, with even more elaborate workarounds.
I liked Go before generics. It had a clear identity. If you wanted to get cute, you could use go generate and generate code. They should've made that much more convenient and ergonomic, if they wanted to make the language more powerful (and the nice thing is that it still sits outside of the language).
I think the point Go was making is that these complex things generally have little use in application code, and 99% of the time they're there for people who want to show how smart they are, at the expense of code readability, and accessibility.
Comment by mrkaye97 20 hours ago
@hatchet.task()
async def my_task(...) -> SomeOutputType:
return SomeOutputType(...)
## imagine this is an API handler:
@api_handler("/some/path")
async def handle() -> ...:
result = await my_task.run(...)
# we now know `result` is of type `SomeOutputType` without any sort of type assertion, etc.
Admittedly, I'm not a Go expert, nor am I a programming languages expert. But I do feel that this type of behavior is really only possible (with nice ergonomics) with generics, and it's always been upsetting to me that somehow Python's type system feels more complete than Go's in this arena, or at least it has until more recently.Maybe this falls into the 1% of cases, but I'd suspect this sort of thing is more common than that.
Edit: I should have mentioned - in the Python example above, `@hatchet.task` is generic with the output type of the task it wraps.
Comment by torginus 19 hours ago
My understanding is that this decorator generates some class in the background that wraps the function into some remotely executable container thing, and handles the networking?
Since python is a dynamic language, and go is not, this would be impossible without codegen, generics or not, but go does have codegen facilities.
And the more immediate implication, that many OOP languages do (and seems to be going on in here) is that they handle asynchrony via some generic Awaitable[T] pattern.
Go does not do this, generally the way you handle asynchrony and abstract typed results is by using channels. You don't await on 'smart' objects, you read from channels (which are 'generic' in a way, but they are the few exceptions where go used to allow this behavior).
Comment by mrkaye97 14 hours ago
Agreed codegen works fine here too by the way, but it feels kind of clunky to me (it’s something I don’t love about Go, although I know it’s also an important part of the ecosystem and is popular).
Comment by sirsinsalot 21 hours ago
Comment by tacitusarc 21 hours ago
Without generics:
‘’’
type Stream[T any] struct{ ... }
func (s Stream[T]) MapInt(f func(T) int) Stream[int] { ... }
func (s Stream[T]) MapString(f func(T) string) Stream[string] { ... }
‘’’
Then later you need to map floats:
‘’’
func (s Stream[T]) MapFloat64(f func(T) float64) Stream[float64] { ... }
‘’’
Then later someone outside the package wants to map a custom type. Too bad! They’ll need to make some custom wrapper that doesn’t follow the pattern.
With the genetics approach the semantics are defined once and usable in all scenarios. You don’t need to keep adding methods; instead the caller can provide the mapping function. Common mapping functions could be pre defined for convenience.
Comment by pkal 1 day ago
Comment by geoka9 22 hours ago
Comment by debugnik 21 hours ago
Now, the verb "map" is the traditional name for this operator, but I agree it sounds confusing when the noun "map" is also a collection type. C# and SQL call this Select, if that helps.
Comment by pkal 22 hours ago
Comment by geoka9 21 hours ago
Comment by kccqzy 21 hours ago
Comment by chenxiaolong 1 day ago
Comment by fooooor 1 day ago
Comment by mappu 1 day ago
Comment by kune 1 day ago
Comment by majewsky 8 hours ago
Which a lot of libraries are doing because they wrote an http.Transport{...} literal in an earlier version, and then std added new fields to the type in a way that silently breaks existing users. The zero value should have matched the previous default behavior.
Case in point: https://github.com/prometheus/client_golang/pull/1885/change...
We had the same in our own library, and now have a testcase checking if our own custom instantiation of http.Transport matches http.DefaultTransport, so that the tests scream loudly when upstream pulls this shit again: https://github.com/sapcc/go-bits/pull/309/changes
Comment by gigatexal 1 day ago
Comment by mappu 1 day ago
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.
Comment by spockz 1 day ago
Anyway, this is why it pays off to read release notes closely and have a decent test suite.
Comment by mxey 1 day ago
Comment by gigatexal 23 hours ago
Comment by MartinodF 1 day ago
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.
Comment by sbstp 1 day ago
Comment by nu2ycombinator 1 day ago
Comment by theplumber 1 day ago
Comment by bessel-dysfunct 1 day ago
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.
Comment by xmprt 1 day ago
Comment by trueno 1 day ago
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 Map<String, List<Map<Integer, Optional<Pair<String, Function<? super List<? extends Comparable<?>>, ? extends Map<String, ?>>>>>>> config;
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 forComment by Cthulhu_ 1 day ago
Comment by twsted 1 day ago
Comment by my-next-account 1 day ago
I really wish they didn't use such stupid LLM-isms.
Comment by neilprosser 1 day ago
"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
Comment by yladiz 1 day ago
Comment by zer00eyz 1 day ago
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.
Comment by abtinf 1 day ago
Comment by jonathrg 1 day ago
Comment by nasretdinov 1 day ago
Comment by jonathrg 1 day ago
Comment by aaa_aaa 1 day ago
Comment by mayama 1 day ago
Comment by Hixon10 1 day ago
Comment by ewy1 1 day ago
Comment by lilbigdoot 1 day ago
Comment by bilinguliar 1 day ago
Comment by melodyogonna 1 day ago
Comment by KolmogorovComp 1 day ago
They're probably useful, but clearly not sexy (as golang in general).
Comment by fweimer 1 day ago
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?
Comment by cherryteastain 1 day ago
Comment by neild 1 day ago
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.
Comment by drivebyhooting 1 day ago
Comment by ad_hockey 1 day ago
"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.
Comment by Cthulhu_ 1 day ago
Comment by fooooor 1 day ago
Comment by kbolino 21 hours ago
Moreover, the "obvious" solution, i.e. treating (T,error) returns the same as Result<T,E>, does not actually work. The problem is that (T,error) behaves like a product type while Result<T,E> is a sum type and yes, some Go code does (ab)use this. In particular, the io.Reader.Read method in the standard library allows implementers to return (n>0,io.EOF), which has no equivalent Result representation. Many people consider this allowance to be a mistake, but it's too late to change it.
Comment by jerf 1 day ago
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.
[1]: https://golangci-lint.run/docs/linters/configuration/#errche...
Comment by drivebyhooting 1 day ago
Comment by jerf 3 hours ago
Still, to really make it work nicely is non-trivial in the presence of things like closures and I couldn't tell you right now how to fix it.
Comment by adrianmsmith 1 day ago
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.
Comment by spockz 1 day ago
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.
Comment by kitd 1 day ago
Comment by adrianmsmith 1 day ago
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.
Comment by mook 1 day ago
Comment by girvo 1 day ago
Comment by Nursie 1 day ago
Thanks so much for that! Now I have no choice but to be reactive when something fails…
Comment by bajsejohannes 1 day ago
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.
Comment by jerf 3 hours ago
The minimal error handling code in Go is really:
if err != nil {
return fmt.Errorf("can't do thing thing I'm trying to do: %w", err)
}
That is actually the code we want to be made easier."if err != nil { return err }" in my code is actually a specific claim that for error-handling purposes this function is conceptually part of the function that is calling it and it has just been factored out for other reasons.
Comment by nitrix 1 day ago
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.
Comment by theplumber 1 day ago
Comment by Hendrikto 1 day ago
Comment by freecodyx 21 hours ago
llm generated
Comment by Altern4tiveAcc 1 day ago
That's completely unreadable.
Comment by asdf88990 1 day ago
Comment by cpuguy83 1 day ago
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.
Comment by fooooor 1 day ago
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.
Comment by nothrows 1 day ago
Comment by EdiX 1 day ago
Comment by adrianmsmith 1 day ago
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, ..).
Comment by foldr 1 day ago
Comment by nirui 1 day ago
I feel the sumtype/emum/routine demanders should yell a little harder so Go team can find their purpose again.
Comment by cookiengineer 1 day ago
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.
Comment by rednafi 1 day ago
> 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.
Comment by stingraycharles 1 day ago
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?
Comment by bradfitz 1 day ago
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.
Comment by amtamt 1 day ago
> 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.
Comment by ckcheng 1 day ago
> 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...
Comment by inigyou 1 day ago
Comment by fragmede 1 day ago
Loads fine for me:
> The bug is in this line:
6: int mid =(low + high) / 2;
Comment by _dain_ 1 day ago
The right way is a + (b - a)/2.
Comment by inigyou 23 hours ago
Comment by fooster 1 day ago
Comment by stingraycharles 1 day ago
am I tearing something down in my comment?
Comment by foldr 1 day ago
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.
Comment by HumblyTossed 1 day ago
Comment by abtinf 1 day ago
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.
Comment by tacitusarc 1 day ago
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”.
Comment by wredcoll 1 day ago
Comment by win311fwg 17 hours ago
Yes. Everyone else knows that they were always expected to be included at some point, as told when Go was first announced to the world: https://www.youtube.com/watch?v=rKnDgT73v8s&t=3267s
> So what changed?
Philip Wadler, of Haskell fame, decided to help? This was well publicized on HN at the time.
> I’m not buying the “it took us 20 years to understand how to do it correctly” argument
If I recall correctly, Pike suggested that they never would have been able to come to that understanding without the outside help. I can understand why you are surprised that it took 20 years for someone with the right expertise to show up. You can get the sense in the above announcement that even the Go team thought that open sourcing the project would attract the right talent far, far earlier. But, now that we have hindsight, we can see that all the experts on HN who could have been the missing positive contributor were too busy complaining that Go didn't have generics. There wasn't enough time left for them to lend a hand. There are only so many hours in the day.
Comment by whateveracct 1 day ago
Comment by eager_learner 1 day ago
Comment by whateveracct 1 day ago
Comment by foldr 1 day ago
Comment by whateveracct 1 day ago
Comment by foldr 1 day ago
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.
Comment by whateveracct 16 hours ago
but the key is they build seamlessly on top of real parametric polymorphism. you still get a real "forall a." in your proposition language.
Go fucked up early on with their core language design due to an inability to learn from the 1970s so now it's terminally dookie sadly
Comment by foldr 8 hours ago
Comment by throw2ih020 1 day ago
The original maintainers moved on to other projects and the new community maintainers came to a consensus through the proposal and governance process.
Comment by bradfitz 1 day ago
Comment by cat-whisperer 1 day ago
Comment by adrianmsmith 1 day ago
Comment by pkal 1 day ago
Comment by kansm 1 day ago
Comment by wredcoll 1 day ago
Comment by wccrawford 1 day ago
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.
Comment by 63stack 19 hours ago
Comment by wccrawford 7 hours ago
If others have posted, it's an indication that it's a wider-spread problem than you knew.
I'd still rather have more information, and I look down on people who only post something that useless. But it does have information I've used to figure problems out.
Comment by smalljelly2018 1 day ago
Comment by fang2hou 1 day ago
Comment by mangudai 1 day ago
Comment by okzgn 1 day ago
Comment by elsaicequeen 22 hours ago
Comment by vladsiu 1 day ago
Comment by pixxxel 1 day ago
Comment by adrianmsmith 1 day ago
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.
Comment by kbolino 1 day ago
Comment by Hendrikto 1 day ago
At runtime, there are only List[int], List[string], etc. List[T] is not a thing anymore.