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.
They're probably useful, but clearly not sexy (as golang in general).
> 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.
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 really wish they didn't use such stupid LLM-isms.
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.
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?
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.
(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)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.
func (b: Box[T]).Map[U: any](f: func(T) -> U) -> Box[U]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.
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.)
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.
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.
There are 37000 programming languages, stop forcing every single one that gets popular to look like this.
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.
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.
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 forAt runtime, there are only List[int], List[string], etc. List[T] is not a thing anymore.
"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.
> 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.
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...
But anyway I find this in Go much more bearable.
"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
// 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.)
Unless you're writing assembler in vim you're not STEM.
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.
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.
The original maintainers moved on to other projects and the new community maintainers came to a consensus through the proposal and governance process.
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.
I think thatโs best as youโll soon learn the โsingle-character capital letter โ generic parameterโ convention
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.
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.
SortBy[T, K comparable](slice: []T, key: func (T) K)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.
Unless you are a compiler/stdlib vendor or contributing to Boost, there are features that you just don't use it daily.
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.
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.
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.
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, ..).
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.
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.
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.
> 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...
am I tearing something down in my comment?
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โ.
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...
Thanks so much for that! Now I have no choice but to be reactive when something failsโฆ
val map : ('a Box) -> ('a -> 'b) -> 'b Box interface Box<T> { value: T }
function map<T, U>(input: Box<T>, func: (value: T) => U): Box<U> {
return { value: func(input.value) }
}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.
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.
Anyway, this is why it pays off to read release notes closely and have a decent test suite.
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.
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.
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.
Also, screw those Romans ;)
Loads fine for me:
> The bug is in this line:
6: int mid =(low + high) / 2;
The right way is a + (b - a)/2.
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.
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.
Go 1.27 is coming soon, so itโs a good time to get a head start on whatโs new. The official release notes are pretty dry, so hereโs a hands-on version with runnable examples showing what changed and how the new behavior works.
A quick credit first: the interactive Go tours were started by Anton Zhiyanov, who wrote one for every release from Go 1.22 through Go 1.26. Heโs decided to stop, so weโre picking up where he left off. His earlier tours are all still worth a read:
Thanks, Anton.
Before we start digging into the new features, letโs set the context.
This article is based on the official release notes and the Go source code, licensed under the BSD-3-Clause. This is not an exhaustive list; see the official release notes for that.
Links point to the documentation (๐), proposals (๐ฃ), most relevant commits (๐๐), and authors (๐) for each feature; check them out for motivation, usage, and implementation details. The authors (๐) are the people who contributed to the feature (writing the implementation, the tests, or, for features that graduated from an earlier experiment, the original design), not necessarily a single main author.
Error handling is often skipped to keep the examples short. Donโt do this in production ใ
[
](#generic-methods)
This is the headline of the release. A method declaration may now declare its own type parameters, independent of the receiverโs. Before Go 1.27, only top-level functions could be generic, so a generic operation on a type had to live as a package-level function instead of a method.
Say we have a generic container and want a Map operation that can change the element type:
type Box[T any] struct{ v T }
// The method declares its own type parameter U (new in Go 1.27).
func (b Box[T]) Map[U any](f func(T) U) Box[U] {
return Box[U]{v: f(b.v)}
}
Now Map is a method of Box and can transform an int box into a string box:
func main() {
b := Box[int]{v: 21}
doubled := b.Map(func(n int) int { return n * 2 })
label := doubled.Map(func(n int) string {
return fmt.Sprintf("value=%d", n)
})
fmt.Println(label.v)
}
value=42
There is one important restriction: interfaces still canโt declare type-parameterized methods, and a generic method canโt be used to satisfy an interface. Put a generic method in an interface and the compiler stops you:
type Mapper interface {
Map[U any](f func(int) U) any // interfaces can't declare generic methods
}
interface method must have no type parameters
[
](#struct-literal-field-selectors)
A key in a struct literal may now be any valid field selector for the struct type, not just a top-level field name. In practice this means you can set a promoted field (one that comes from an embedded struct) directly, without spelling out the embedded type.
type Base struct {
ID int
}
type User struct {
Base
Name string
}
Before Go 1.27 you had to write User{Base: Base{ID: 7}, Name: "Mittens"}. Now the promoted ID works as a key on its own:
u := User{ID: 7, Name: "Mittens"}
fmt.Println(u.ID, u.Name)
7 Mittens
[
](#generalized-function-type-inference)
Function type inference has been generalized to apply in all contexts where a generic function is used where a matching function type is expected: not just plain assignment to a variable (which already worked), but also conversions and composite literals. In those cases you previously had to spell out the type arguments by hand.
Take two generic helpers and drop them into a slice whose element type is func([]int) int:
func first[T any](s []T) T { return s[0] }
func last[T any](s []T) T { return s[len(s)-1] }
// The slice's element type drives inference: T=int for each entry.
// Before Go 1.27 this failed with "cannot use generic function
// without instantiation"; you had to write first[int], last[int].
ops := []func([]int) int{first, last}
for _, op := range ops {
fmt.Println(op([]int{10, 20, 30}))
}
10
30
[
](#faster-memory-allocation)
The compiler now generates calls to size-specialized memory allocation routines, cutting the cost of some small (under 80 bytes) allocations by up to 30%. Improvements vary with the workload, but the overall gain is expected to be around 1% in real allocation-heavy programs. The tradeoff is about 60 KB of extra binary size, independent of the workload.
Thereโs nothing to change in your code; it just gets a little faster. If you need to turn it off, build with GOEXPERIMENT=nosizespecializedmalloc. That opt-out is expected to be removed in Go 1.28.
[
](#goroutine-labels-in-tracebacks)
For modules whose go.mod sets Go 1.27 or later, tracebacks now include runtime/pprof goroutine labels in the header line of each goroutine. If you already attach labels for profiling with pprof.Do, that context now shows up in crash dumps, SIGQUIT traces, and runtime.Stack output too (handy for telling apart otherwise identical goroutines).
Here we attach a label, then dump the current goroutineโs stack to see it in action:
ctx := context.Background()
pprof.Do(ctx, pprof.Labels("request", "42"), func(ctx context.Context) {
buf := make([]byte, 1<<12)
n := runtime.Stack(buf, false)
fmt.Printf("%s", buf[:n])
})
goroutine 1 [running] {request: 42}:
main.main.func1(...)
.../main.go:14 +0x38
runtime/pprof.Do(...)
.../runtime/pprof/runtime.go:57 +0x8c
main.main()
.../main.go:12 +0x6c
The pointer arguments, offsets, and file paths differ from run to run; whatโs new is the {request: 42} appended right after the goroutineโs [running] state: its pprof labels. That same {...} annotation appears on the header of every labeled goroutine in a panic or SIGQUIT traceback. You can disable it with GODEBUG=tracebacklabels=0 (the setting was added in Go 1.26). The opt-out is expected to stay indefinitely, in case labels carry sensitive data you donโt want in tracebacks.
[
](#goroutine-leak-profile)
Go 1.26 introduced a goroutine leak detector as an experiment. In Go 1.27 it graduates to a regular profile: runtime/pprof exposes a goroutineleak profile that runs a GC cycle to find goroutines that are permanently blocked (leaked) and reports their stacks; no GOEXPERIMENT needed anymore.
A โleakedโ goroutine is one blocked forever on a channel, mutex, or similar, with no way to ever make progress. The classic example is a goroutine that sends to a channel it alone holds, so nobody can ever receive from it:
func leak() {
ch := make(chan int) // only this goroutine ever sees ch
ch <- 1 // blocks forever: nobody will ever receive
}
Start one, let it park, then dump the profile:
go leak() // this goroutine can never finish
runtime.Gosched() // let it park on the send
// The GC-backed scan finds goroutines that can never make progress.
pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
goroutineleak profile: total 1
1 @ 0x... 0x... 0x... 0x... 0x...
# 0x... main.leak+0x27 .../main.go:11
The total 1 line says the detector found exactly one leaked goroutine, and the stack pins it to main.leak: the ch <- 1 send that will never complete (the addresses vary from run to run). In a real service youโd usually scrape the /debug/pprof/goroutineleak net/http/pprof endpoint instead of writing to stdout.
[
](#post-quantum-signatures)
The new crypto/mldsa package implements ML-DSA, the post-quantum digital signature scheme specified in FIPS 204. It comes in three parameter sets (MLDSA44, MLDSA65, and MLDSA87), trading key/signature size for security level.
priv, _ := mldsa.GenerateKey(mldsa.MLDSA65())
msg := []byte("victoria metrics")
sig, _ := priv.Sign(rand.Reader, msg, crypto.Hash(0))
fmt.Println("scheme: ", mldsa.MLDSA65())
fmt.Println("sig size:", mldsa.MLDSA65().SignatureSize())
fmt.Println("verified:", mldsa.Verify(priv.PublicKey(), msg, sig, nil) == nil)
scheme: ML-DSA-65
sig size: 3309
verified: true
ML-DSA support also reaches crypto/x509 (private keys, public keys, and signatures) and crypto/tls (the new MLDSA44, MLDSA65, and MLDSA87 signature schemes in TLS 1.3).
[
](#the-uuid-package)
Go finally has a UUID package in the standard library. The new top-level uuid package generates and parses UUIDs per RFC 9562, using a cryptographically secure random source. Random-component UUIDs are comparable, so you can use == on them directly.
a := uuid.MustParse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
fmt.Println("parsed:", a)
fmt.Println("nil: ", uuid.Nil())
fmt.Println("max: ", uuid.Max())
parsed: f81d4fae-7dec-11d0-a765-00a0c91e6bf6
nil: 00000000-0000-0000-0000-000000000000
max: ffffffff-ffff-ffff-ffff-ffffffffffff
For generation, uuid.New() picks an algorithm suitable for most uses, while uuid.NewV4() gives a purely random UUID and uuid.NewV7() gives a time-ordered one; the latter is great for database keys because it sorts by creation time. Each call produces a fresh value, so try running this a few times:
fmt.Println(uuid.NewV4()) // random
fmt.Println(uuid.NewV7()) // time-ordered
[
](#json-v2-by-default)
The long-awaited encoding/json/v2 rewrite has been experimental since Go 1.25. In Go 1.27 the experiment graduates: encoding/json/v2 and its low-level companion encoding/json/jsontext are now available without the GOEXPERIMENT=jsonv2 build flag. The quieter but bigger change: the classic encoding/json (v1) package is now backed by the v2 implementation under the hood.
The switch is transparent: behavior is preserved (only some error-message text differs), with new options pinning v2 to v1 semantics where theyโd otherwise diverge. No migration is required, and GOEXPERIMENT=nojsonv2 restores the original v1 implementation if you hit a compatibility issue.
For the common case, the v2 API mirrors v1 (the import here is json "encoding/json/v2"):
type Point struct {
X int `json:"x"`
Y int `json:"y"`
}
data, err := json.Marshal(Point{X: 1, Y: 2})
fmt.Println(string(data), err)
{"x":1,"y":2} <nil>
One behavior worth knowing: unlike v1, which always sorts map keys, v2 does not sort them by default; skipping the sort is faster. When you need stable map output (for golden tests, say), pass the json.Deterministic option.
[
](#portable-simd)
Go 1.27 adds an experimental simd package: portable, vector-size-agnostic SIMD that compiles down to real hardware vector instructions where theyโre available and falls back to a pure-Go emulation where they arenโt. Itโs off by default; you build with GOEXPERIMENT=simd to enable it.
The types are named after their element type with an s suffix (Int32s, Float32s, Float64s, and so on), and their width is deliberately not fixed: a Float32s might hold 4 lanes on one machine and 16 on another. You load a vector from a slice, operate on it, and store it back, letting the hardware pick the width:
a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
b := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160}
va := simd.LoadFloat32s(a) // reads exactly va.Len() lanes from a
vb := simd.LoadFloat32s(b)
sum := va.Add(vb) // element-wise add, many lanes in one instruction
out := make([]float32, sum.Len())
sum.Store(out)
fmt.Println(out[:4])
[11 22 33 44]
[
](#cut-around-the-last-separator)
strings.Cut (from Go 1.18) splits around the first occurrence of a separator. Go 1.27 adds strings.CutLast (and bytes.CutLast) for the last occurrence (a cleaner replacement for many LastIndex dances).
before, after, found := strings.CutLast("a/b/c", "/")
fmt.Printf("%q %q %v\n", before, after, found)
before, after, found = strings.CutLast("nosep", "/")
fmt.Printf("%q %q %v\n", before, after, found)
"a/b" "c" true
"nosep" "" false
As with Cut, when the separator isnโt found you get the whole input as before, an empty after, and found == false.
[
](#generic-hashing)
The hash/maphash package gains a Hasher[T] interface: a contract that future hash-based data structures (hash tables, Bloom filters, and so on) can use to hash and compare values of a type. It bundles two operations: Hash, which mixes a value into a running hash, and Equal, which compares two values. The rule tying them together is that equal values must hash the same.
Thereโs a ready-made ComparableHasher[T] (hash by value, equality by ==) for any comparable type, but the interesting part is defining your own. Hereโs a case-insensitive string hasher:
type ciHasher struct{}
// Equal ignores case; Hash mixes in the lower-cased form, so values
// that are Equal always hash the same.
func (ciHasher) Hash(h *maphash.Hash, s string) { h.WriteString(strings.ToLower(s)) }
func (ciHasher) Equal(x, y string) bool { return strings.EqualFold(x, y) }
Now "Go" and "GO" count as equal and hash identically, which plain == and value hashing canโt do:
var h maphash.Hasher[string] = ciHasher{} // plug in the custom strategy
fmt.Println(h.Equal("Go", "GO"), h.Equal("Go", "Rust"))
// Equal values must hash the same, so feed each into a Hash sharing one seed:
seed := maphash.MakeSeed()
var a, b maphash.Hash
a.SetSeed(seed)
b.SetSeed(seed)
h.Hash(&a, "Go")
h.Hash(&b, "GO")
fmt.Println(a.Sum64() == b.Sum64())
true false
true
[
](#integer-division-with-rounding)
math/big adds Int.Divide, which computes a quotient and remainder together with an explicit rounding mode: Trunc, Floor, Round, or Ceil. The classic Quo/Mod always truncates toward zero, so this fills a real gap for financial and numeric code.
x, y := big.NewInt(7), big.NewInt(2)
q, r := new(big.Int), new(big.Int)
q.Divide(x, y, r, big.Ceil)
fmt.Printf("ceil: q=%s r=%s\n", q, r)
q.Divide(x, y, r, big.Floor)
fmt.Printf("floor: q=%s r=%s\n", q, r)
ceil: q=4 r=-1
floor: q=3 r=1
Notice how the remainder follows the rounding mode: with Ceil the quotient rounds up to 4, leaving a remainder of โ1; with Floor it rounds down to 3, leaving 1.
[
](#random-numbers-your-type)
math/rand/v2 has had a top-level generic N function since Go 1.22. Go 1.27 adds it as a method, (*Rand).N, so you can draw a bounded random number of any integer or duration type from your own *Rand source.
r := rand.New(rand.NewPCG(1, 2)) // fixed seed โ reproducible
fmt.Println(r.N(100)) // int in [0, 100)
76
[
](#sleep-in-synthetic-time)
testing/synctest (stable since Go 1.25) lets you test concurrent code against a fake clock. Go 1.27 adds a Sleep helper that combines time.Sleep with synctest.Wait: advance the bubbleโs synthetic clock and then wait for all goroutines to settle, in one call.
Inside a bubble the time package uses a fake clock, so a two-second sleep returns instantly; synctest.Sleep also waits for the background goroutine to finish before moving on:
t := &testing.T{} // in real code, use the *testing.T your test receives
synctest.Test(t, func(t *testing.T) {
start := time.Now()
go func() {
time.Sleep(time.Second)
fmt.Println("worker woke at", time.Since(start))
}()
// Advance fake time by 2s AND wait for goroutines to settle, in one call.
synctest.Sleep(2 * time.Second)
fmt.Println("main advanced", time.Since(start))
})
worker woke at 1s
main advanced 2s
Both durations are exact; no real time passes. Itโs a small convenience, but it removes a common two-line boilerplate from almost every synctest-based test. (The bare &testing.T{} above is only to make the snippet self-contained; in a real test synctest.Sleep lives inside a func TestXxx(t *testing.T) and you pass that t.)
[
](#in-memory-test-servers)
httptest.NewTestServer creates an httptest.Server backed by an in-memory fake network instead of a real TCP listener. No real ports are involved, and it registers its own cleanup via t.Cleanup, so thereโs no defer srv.Close() to remember. It also pairs with testing/synctest, letting HTTP round-trips run in synthetic time for faster, fully deterministic tests.
t := &testing.T{} // in real code, use the *testing.T your test receives
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello from the in-memory server")
})
srv := httptest.NewTestServer(t, handler) // in-memory network, auto-cleanup
resp, _ := srv.Client().Get(srv.URL) // no real TCP port
body, _ := io.ReadAll(resp.Body)
fmt.Print(string(body))
hello from the in-memory server
The request never touches the network stack; srv.Client() is wired straight to the handler over an in-process pipe. (As with the previous example, the bare &testing.T{} is only to keep the snippet self-contained; in a real test youโd pass the t from your func TestXxx(t *testing.T).)
[
](#unicode-17)
The unicode package and the rest of the standard library have been upgraded from Unicode 15 to Unicode 17, picking up new scripts, characters, and properties.
To see the jump in action, take ๐ซ (U+1FADC โroot vegetableโ), which was added in Unicode 16.0. On Goโs old Unicode 15 data it was an unassigned code point, so IsSymbol and IsGraphic both returned false; now itโs a recognized symbol:
fmt.Println("Unicode", unicode.Version)
r := '๐ซ' // U+1FADC "root vegetable", added in Unicode 16.0
fmt.Printf("%#U symbol=%v graphic=%v\n", r, unicode.IsSymbol(r), unicode.IsGraphic(r))
Unicode 17.0.0
U+1FADC '๐ซ' symbol=true graphic=true
On Go 1.26 the very same code prints Unicode 15.0.0 and U+1FADC symbol=false graphic=false; the code point isnโt even printable, so %#U omits the glyph.
[
](#other-notable-changes)
A few smaller changes that are easy to miss but can affect real code:
time channels are now always unbuffered. Following the timer rework in Go 1.23, the channels returned by time.After, time.NewTimer, time.NewTicker, and friends are now synchronous in every case. The asynctimerchan GODEBUG that restored the old buffered behavior has been removed, so if you relied on asynctimerchan=1, that escape hatch is gone.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; if you were leaning on an early Close to abort a large download, set Transport.DisableKeepAlives to opt out.Server.DisableClientPriority = true to restore the old round-robin behavior.crypto/x509 honors SSL_CERT_FILE and SSL_CERT_DIR on Windows and macOS. When either is set, SystemCertPool loads roots from disk and uses Goโs own verifier instead of the platform APIs (disable with GODEBUG=x509sslcertoverrideplatform=0).[
](#tooling)
A grab bag of go command and toolchain improvements:
go test runs the stdversion vet check by default. It reports uses of standard library symbols that are newer than the Go version declared in your go.mod, catching accidental โworks on my machineโ version drift.go doc pkg@version. You can now ask for documentation at a specific module version, e.g. go doc example.com/mod@v1.2.3 (proposal 63696).go doc -ex. The new -ex flag lists a packageโs runnable examples (go doc -ex bytes). To print one exampleโs source, name it directly, e.g. go doc bytes.ExampleBuffer.go fix gains new modernizers. The atomictypes, embedlit, slicesbackward, and unsafefuncs analyzers rewrite older patterns to their modern equivalents. (The waitgroup analyzer was renamed to waitgroupgo, and fmtappendf was dropped.)go mod tidy tidies require blocks. For modules on go 1.27 or later, it now merges scattered require blocks into the canonical two (one for direct and one for indirect dependencies) while preserving attached comments.go tool trace -http=:6060 binds to localhost. When given only a port, the trace UI now listens on localhost only, matching go tool pprof. Pass an explicit address to listen more broadly.go command dropped support for the Bazaar (bzr) version control system (proposal 78090).@file) are now supported by the compile, link, asm, cgo, cover, and pack tools, compatible with GCCโs format (helpful for build systems that hit command-line length limits).[
](#hidden-gems)
Everything above comes from the release notes. But the notes are a curated summary, and roughly 1,600 commits landed between Go 1.26 and Go 1.27. Here are some changes that are worth knowing about:
HTTP/2 is finally a real package. For years, net/httpโs HTTP/2 support lived in h2_bundle.go: a single 12,226-line file, mechanically generated by concatenating golang.org/x/net/http2 and prefixing every identifier with http2. In Go 1.27 itโs gone, replaced by an actual package, net/http/internal/http2. ๐ฃ 67810 โข ๐๐ c5f43ab, 080aa8e
HTTP/3 is quietly taking shape. Go 1.27 adds unexported, pluggable HTTP/3 hooks to net/http, and teaches much of the net/http test suite to run against HTTP/3. Nothing is exported yet, so thereโs nothing to call, but the scaffolding for a future http.Transport that speaks QUIC is now in the tree. ๐ฃ 77440 โข ๐๐ 0b9bcbc, 96d6d38, db6661a
Three new compiler optimizations, all on by default. A known bits dataflow pass tracks which bits of a value are provably 0 or 1 and folds away the resulting redundancy; loop-invariant code motion moves computations whose result never changes out of the loop, so they run once instead of on every iteration; and switch statements now compile to lookup tables where the cases allow it, including with fallthrough. ๐๐ 7a8dcab, f9f351b, 9c688e3, 2a902c8, 1f5c165
The runtime already uses the new SIMD package. The simd package is presented as an experiment for your code, but the standard library is already a customer: the Swiss Table map implementation reimplemented its MemHash32, MemHash64, and StrHash functions on top of simd/archsimd intrinsics. ๐๐ 2403e59, 252a8ad
Reorganized type metadata in the linker. Type descriptors and itabs moved into a dedicated .go.type section with explicit alignment, both typelinks and itablinks were removed outright, and descriptor size arithmetic was centralized in internal/abi. One consequence to watch for: reflect.typelinks now returns types rather than offsets, and thatโs a symbol some libraries reach through //go:linkname. ๐๐ 13096a6, 481ab86, 6ef7fe9, 3390ec5, 87fae36
Unsanctioned //go:linkname gets harder. A new linknamestd directive marks linknames that only the standard library may pull, cmd/link now checks linkname access to assembly symbols, and export linknames were added across the tree for assembly symbols reached from other packages. If you depend on a linkname that Go never blessed, 1.27 is a good release to test against early. ๐๐ 46755cb, aee6009, 4dde0f6
A new experimental map memory layout. GOEXPERIMENT=mapsplitgroup changes the layout of a map group from interleaved key/value slots (KVKVKVKV) to split key and value arrays (KKKKVVVV). Itโs off by default. ๐๐ 5560073
os.Root closed another escape. ReadDir and Readdir could be used to escape a root. Worth flagging because os.Root is young and marketed as a containment boundary. ๐๐ 657ed93
[
](#final-thoughts)
Go 1.27 is a meaty release whose center of gravity is the type system. A few themes stand out:
simd package opens the door to explicit vectorization.crypto/mldsa, crypto/x509, and crypto/tls.uuid package, json/v2 graduating out of the experiment, CutLast, and nicer testing helpers.All in all, a strong release; a reminder that Goโs โboring on purposeโ pace still delivers a lot each cycle.
P.S. Curious how we use Go at scale? The whole VictoriaMetrics stack (metrics, logs, and traces) is written in Go. Browse the rest of our blog for deep dives into the runtime, the standard library, and performance.