top | item 30205232

What I'd like to see in Go 2.0

215 points| AtroxDev | 4 years ago |sethvargo.com

219 comments

order

mieubrisse|4 years ago

I'd add one more to this list: proper enum types.

We use enums heavily to force devs who use our code into good choices, but the options are currently:

1) Use int-type enums with iota: no human-readable error values, no compile-time guard against illegal enum values, no exhaustive `switch`, and no autogenerated ValidEnumValues for validation at runtime (we instead need to create ValidEnumValues and remember to update it every time a new enum value is added).

2) Use string-type enums: human-readable error values, but same problems with no compile-time guards, exhaustive switch, or validation at runtime.

3) Use struct-based enums per https://threedots.tech/post/safer-enums-in-go/ : human-readable error values and an okay compile-time check (only the all-default-values struct or the values we define), but it still doesn't have exhaustive switch, is a complex pattern so most people don't know to use it, and suffers from the mutable `var` issues the post author detailed.

To my naive eye, it seems like a built-in, compile time-checked enum type with a FromString() function would help the community tremendously.

slx26|4 years ago

Just adding to the discussion:

I find this comment from Griesemer [0] on one of the github issues for enums in Golang quite insightful:

>> [...] all the proposals on enums I've seen so far, including this one, mix way too many things together in my mind. [...] Instead, I suggest that we try to address these (the enum) properties individually. If we had a mechanism in the language for immutable values (a big "if"), and a mechanism to concisely define new values (more on that below), than an "enum" is simply a mechanism to lump together a list of values of a given type such that the compiler can do compile-time validation.

Like with generics, I like the team's approach of taking features seriously, not adding them just because other languages have them, but actually trying to figure out a way for them to work in Go, as cleanly as possible. I think computer science, as a field, benefits from this approach.

And I also dislike many things from Go, and I want "enums" badly too, but that's for another comment.

[0] https://github.com/golang/go/issues/28987#issuecomment-49679...

cle|4 years ago

Personally I’ve run into more problems with strict enum types in distributed systems in a team setting, than I have with Go’s lack of them. In that setting, strict enums are usually over-strict and eventually you back yourself into a corner in terms of being able to roll out new enum values.

When there’s no clear winner in terms of tradeoffs, I prefer to leave it out of the language like Go has done.

mseepgood|4 years ago

I don't think you would need a 2.0 (backward-incompatible language change) for any of this.

masklinn|4 years ago

> We use enums heavily to force devs who use our code into good choices

Beware, tho, that with many languages today you’re not really doing that even when they advertise enums e.g. in both C# and C++, enums are not type-safe (not even `enum class`). Iota is, at least, a fair acknowledgement of that.

> with a FromString() function

That seems like way a step too far, is there any such “default method” today? And I don’t think Go has any sort of return-type overloading does it?

ibraheemdev|4 years ago

The type sets proposal for Go has already been accepted as part of the generics proposal [0]:

    type SignedInteger interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64
    }
Interfaces that contain type sets are only allowed to be used in generic constraints. However, a future extension might permit the use of type sets in regular interface types:

> We have proposed that constraints can embed some additional elements. With this proposal, any interface type that embeds anything other than an interface type can only be used as a constraint or as an embedded element in another constraint. A natural next step would be to permit using interface types that embed any type, or that embed these new elements, as an ordinary type, not just as a constraint.

> We are not proposing that today. But the rules for type sets and methods set above describe how they would behave. Any type that is an element of the type set could be assigned to such an interface type. A value of such an interface type would permit calling any member of the corresponding method set.

> This would permit a version of what other languages call sum types or union types. It would be a Go interface type to which only specific types could be assigned. Such an interface type could still take the value nil, of course, so it would not be quite the same as a typical sum type.

> In any case, this is something to consider in a future proposal, not this one.

This along with exhaustive type switches would bring Go something close to the sum types of Rust and Swift.

[0]: https://github.com/golang/go/issues/45346

_0w8t|4 years ago

Another possibility is to use boolean flags. Of cause the compiler then will not enforce that only one of the flags is set. On the other hand on few occasions I observed how initial design with disjoint cases evolved into cases that can be set at the same time modeled with flags.

rplnt|4 years ago

> no exhaustive `switch`

This is what linters will do for you by default.

> we instead need to create ValidEnumValues and remember to update it every time a new enum value is added

Code generators are first class citizens in go, and writing an icky but reliable test won't be too hard either.

donatj|4 years ago

> Use int-type enums with iota: […] no compile-time guard against illegal enum values

Create a new int type and use that for your enums. While you still can create an illegal enum value, you basically have to be looking for trouble. It’s not going to happen accidentally. It’s even harder if it’s an unpunished type in a different package.

See:

https://github.com/donatj/sqlread/blob/91b4f07370d12d697d18a...

AtroxDev|4 years ago

From Rob Pike on reddit regarding this post[0]:

The and and or functions in the template packages do short-circuit, so he's got one thing already. It was a relatively recent change, but it's there.

Non-deterministic select is a critical detail of its design. If you depend on a deterministic order of completion of tasks, you're going to have problems. Now there are cases where determinism might be what you want, but they are peculiar. And given Go's general approach to doing things only one way, you get non-determinism.

A shorthand syntax for trial communication existed. We took it out long ago. Again, you only need one way to do things, and again, it's a rare thing to need. Not worth special syntax.

Some of the other things mentioned may be worth thinking about, and some of them have already (a logging interface for instance), and some we just got wrong (range). But overall this seems like a list of things driven by a particular way of working that is not universal and discounts the cost of creating consensus around the right solutions to some of these problems.

Which is not to discount the author's concerns. This is a thoughtful post.

0: https://old.reddit.com/r/golang/comments/s58ico/what_id_like...

dewey|4 years ago

> Again, you only need one way to do things, and again, it's a rare thing to need. Not worth special syntax.

This really is one of the parts I like the most about Go. It really makes so many things simpler. Discussing code, tutorials and writing it.

Every time I'm trying to do something in JS I have to figure out why every guide has a different way of achieving the same thing and what are the implementation differences.

tapirl|4 years ago

> but (deterministic-select cases) hey are peculiar.

It looks for most select blocks in Go code, it doesn't matter whether or not they are non-deterministic or deterministic.

But, if the default is deterministic, user code could simulate non-deterministic, without much performance loss. Not vice versa (the current design).

t43562|4 years ago

Everyone has their own gripes. Modules are what cause me the most pain in Go - especially where they're in github and I need to fork them and now change all the code that references them. I don't know if the problems are even tractable because the way it all works is so incredibly complicated and any change would break a lot.

I would like to remove all the "magic" that's built-in for specific SCMS/repository hosting services and have something that operates in a simple and predictable manner like C includes and include paths (although obviously I don't like preprocessing so not that).

As for the language, I like the range reference idea but my own minor pet peeve is an issue with pre-assignment in if-statements etc which makes a neat feature almost useless:

  // This is nice because err only exists within the if so we don't have to 
  // reuse a variable or invent new names both of which are untidy and have potential 
  // to cause errors (esp when copy-pasting):
  if err := someoperation(); err != nil; {
    // Handle the error
  }


  // This however won't work:  
  func doThing() string {    
     result := "OK" 
     
      if result, err := somethingelse(); err != nil { // result does not need to be created but err does so we cannot do this.
          return "ERROR"  
      }
    

   return result
  }
I don't have any good ideas about how to change the syntax unfortunately.

fastest963|4 years ago

You should be able to use "replace" to use your forked module instead of the original and you don't have to change anything.

usrbinbash|4 years ago

Easily fixed using lexical scopes:

    func doThing() string {
        result := "OK"

        {
            var err error
            if result, err = somethingElse(); err != nil {
                return "ERROR"
            }
        }

        return result
    }
`err` is introduced in the lexical scope, `result` isn't so it still refers to the string from the surrounding scope. `err` does not pollute the surrounding scope.

You can also try the complete version here: https://go.dev/play/p/kDEB11YdvSs

assbuttbuttass|4 years ago

The fix is pretty simple, just declare err ahead of time:

    func doThing() string {    
        result := "OK"
        var err error
        if result, err = somethingelse(); err != nil {
            return "ERROR"  
        }

        return result
    }

jatone|4 years ago

yeah it sucks, I think IDEs should help here. 1 click uplift var error to parent scope.

maccard|4 years ago

This is a great post, and I agree with much of what he said (range shouldn't copy - I would love a range that iterates by const-reference by default, to lift a phrase from C++).

Deterministic select I hard disagree with. The code in the blog post is race-y, and needs to be fixed, not select. If anything, making select deterministic will introduce _more_ subtle bugs when developers rely on that behavior only to find out in the real world that things aren't necessarily as quick as they are in development.

sethvargo|4 years ago

How would you fix that code?

the_gipsy|4 years ago

Proper sum types, if only for proper error handling. The if err != nil dance is extremely verbose and error prone with consecutive checks.

Jeff_Brown|4 years ago

That could also alleviate the no-exhaustive-enum-checking problem mentioned in another top-level comment.

ternaryoperator|4 years ago

Mine would be a much larger collections library. Having come to go from Java and finding that the standard go library has no trees, no stack, no skip list, etc. was quite a surprise. Possibly the advent of generics will stimulate development of a more robust standard collections library.

christophilus|4 years ago

It does have a doubly-linked list which can easily serve as a stack, though. And it has a heap which is a poor man's replacement for some uses of tree. I've found that I can get quite a bit farther with the built-in Go slices, maps, and lists than I thought.

But yeah. Now that generics are in, I do hope they add a handful of common collections.

[0] https://pkg.go.dev/container/list@go1.17.6

[1] https://pkg.go.dev/container/heap@go1.17.6

[2] https://pkg.go.dev/container/ring@go1.17.6

JulianMorrison|4 years ago

> Alternatively, Go 2.0 could implement "frozen" global variables

A more general change would be to implement the "var" and "val" distinction that exists in some languages.

    const x = 1 // x is a compile time alias for the untyped abstract number 1
    var x := 1  // define x at runtime to be (int)1, x is mutable
    val x := 1  // define x at runtime to be (int)1, x is immutable
Then the globals can be defined with "val".

maerF0x0|4 years ago

could be interesting, however I'd hope for something more visually distinctive that val/var as it took about 2-3 reads for me notice what was even the diff between L2 and L3.

bruce343434|4 years ago

the val and const cases should hardly be different if the compiler has constant folding, except maybe for the typing.

avgcorrection|4 years ago

I don’t understand why Scala chose “var” for mutable variables. A variable is not defined by being mutable—it is defined by being variable, i.e. not a constant. And it is immutable in math (where we don’t have to care about performance). So “val” is also a “var”, conceptually.

Eun|4 years ago

I would like to add proper JSON5 support.

The template problem is a real problem. I used it once and it was pain and moved away instantly. I would vote for go inside go as a template system. So you can effectively write go code.

With the help of yaegi[1] a go templating engine can be build e.g here[2].

[1]: https://github.com/traefik/yaegi

[2]: https://github.com/Eun/yaegi-template

jerf|4 years ago

You don't need official Go support for JSON5. Same for templating. The templating library in the standard library isn't anything special, it's just in the standard library. If you don't like it, go get one of the dozens of others on offer.

I think there's a number of language communities you can "grow up" in that teach you that things in the standard library are faster than anything else and have had more attention paid to them than anything else, like Python or Perl, because those languages are slow, and things going into the standard library have generally been converted to C. I think that because I look in my own brain and find that idea knocking about even though nobody ever said it to me directly. But that's not true in the compiled languages, of which Go is one. The vast majority of the Go standard library is written in Go. The vast majority of that vast majority isn't even written in complicated Go, it's just in Go that any Go programmer who has run through a good tutorial can read, it's not like a lot of standard libraries that are fast because they are written impenetrably. (Although the standard library may have subtle performance optimizations because it chooses this way of writing it in Go rather than that way, it's still almost entirely straightforward, comprehensible code.)

If you want JSON5 or a different template library, go get or write one. The Go encoding/json or text/template don't use any special access or have any magical compiler callouts you can't access in a library or anything else; if you grab JSON5 library (if such a thing exists), you've got as much support for it as JSON or text/template.

It's even often good that it's not in the standard library. I've been using the YAML support a lot lately. The biggest Go YAML library is on major version 3, and both of the jumps from 1 to 2 and 2 to 3 were huge, and would have been significantly inhibited if v1 was in the standard library and the backwards compatibility promise applied. v1 definitely resembles encoding/json, but that's missing a lot of things for YAML. If Go "supported YAML" by having v1 in the standard library, everybody would be complaining that it didn't support it well, and by contrast, asking anyone to jump straight to the v3 interface would be an insanely tall ask to do on the first try without any experimentation in the mean time. And I'm not even 100% sure there won't be a v4.

honkycat|4 years ago

What I would like to see:

- Enun Types

- A special operator to cut down on `if err != nil { return err }` and just return at that point.

- Named arguments, and optional params with defaults

- Default values on structs

- ...macros? We already have `go generate ./...`

( edit: Removed unformatted source code )

Jeff_Brown|4 years ago

Sum types would satisfy the first two on that list, as well as making an ergonomic optional type trivial to define.

sdevonoes|4 years ago

I would add: an extended standard library for "common stuff". I don't want to import a third-party library nor write my own "utils.go" to do:

    func contains(s []int, e int) bool {
        for _, a := range s {
            if a == e {
                return true
            }
        }
        return false
    }

Mawr|4 years ago

If you need to look up or worse, delete, a value in a slice, then you probably shouldn't be using a slice in the first place. You probably want to replace your slice with a set.

cpeterso|4 years ago

Two Go 2 proposals that interested me were:

* nillability annotations: https://github.com/golang/go/issues/49202

* Change int from a machine word size (int32 or int64) to arbitrary precision (bigint): https://github.com/golang/go/issues/19623

Sadly the nillability annotations were rejected because they weren't backwards compatible. The bigint change is also unlikely to be accepted because the issue is already five years old and there are concerns about performance.

usrbinbash|4 years ago

If this was implemented:

    func foo(a, b int) int {
        return a * b
    }
At compile time, what is the return type of foo? Answer: Unknown. The compiler has no way of determining if the resulting type will be small or big int. This has to be taken into account not just in foo, but in every function calling foo, and every function that is called with the return of foo as a param.

One of the best things about golang, is how little "magic" there is. Everything that happens is immediately obvious from the code. An int that is upgraded to a completely different type when it reaches certain values, goes directly counter to that; it would be hidden behaviour, not immediately obvious from the code.

Should golang have a builtin arbitrary sized integer type? Maybe. Using math/big can become cumbersome. But a much better solution would be to introduce a new builtin type like `num`

mountainriver|4 years ago

I would really love to see default parameters and struct values. I jump between Python and Go in my day job and Go is a much better language overall but things like this make it painful

geenat|4 years ago

This is also my #1.

Parameter / Option ergonomics.

The current best practice of "functional options" and long function chains results in far too many function stubs ... its a minimum of 3 extra lines per parameter. Parameter structs require a whole extra struct...

Borrowing optional / named parameters from Python would cut down length and complexity of Go code drastically.

Jeff_Brown|4 years ago

Default parameters have always struck me as dangerous. (And for my day job I use Python.)

sdevonoes|4 years ago

A better template library would be a killer feature. Similar to what PHP has been since forever; the ability to write HTML using all the Go language's features.

geenat|4 years ago

I'd like to see ergonomics improvements, particularly to function parameters.

The current best practice of "functional options" and long function chains results in far too many function stubs ... its a minimum of 3 extra lines per parameter. Parameter structs require a whole extra struct.

Suggestion: Just borrow named / optional parameters from Python. It would cut down length and complexity of Go code drastically.

christophilus|4 years ago

This was a surprisingly good list. Most of these kinds of articles just consist of someone bemoaning the fact that Go isn't Haskell or whatever language they like more. But this is a legitimate list of things that could and should (in my opinion) be changed without turning Go into not-Go.

crnkofe|4 years ago

Oddly enough the list doesn't resonate much with me.

I'd love to see better mocking support. Doing mock.On("fun name", ...) is so backwards, confusing and brittle. It's also a great source of confusion for teammates when tests fail.

I miss better transaction management. I regularly juggle db, transactions and related interfaces and it's a continuous pain.

Then there's the "workaround" for enforcing interface implementation: _ InterfaceType = &struct . This could be easily part of struct def. rather than having it in var section.

As was mentioned by others doing x := 5 only to later do JsonField: &x is just a waste of intellectual firepower. Maybe this can be alleviated by generics but the lang should be able to make this a one liner.

qaq|4 years ago

I have much smaller ask struct type elision in function calls

masklinn|4 years ago

I have to say I’ve no idea what you mean. In function definitions I’d interpret it as type inference (and would disagree) but you specifically talk about function calls, and consider it a small change. Can you describe what you’re thinking of?

randallsquared|4 years ago

The issue with the order of range seems like using the same name for satisfying a different requirement: in a template, you're much more likely to want the value than the index, so it makes sense that a looping construct with a single parameter would be putting the value in that parameter. In a loop in normal code, you're more likely to want to do math on the index. So, I'd say the problem is more about punning the name of these two behaviors than the behavior itself being bad.

masklinn|4 years ago

> In a loop in normal code, you're more likely to want to do math on the index.

It really is not, no. The number of loops using `enumerate` (or working on range / indices directly) in Python or Rust are a small fraction of those just iterating the sequence itself.

That would be even more so for Go, which has no higher-level data-oriented utilities (e.g. HOFs or comprehensions, which would usually replace a number of non-indexed loops, and would thus increase the ratio of indexed to non-indexed loops).

bstpierre|4 years ago

Serious question: what are the odds that go 2 ends up like python 3 and it takes the world over a decade of pain to migrate? (I like both python and go, and I’m still maintaining a sizable body of py2 code.)

“Backwards compatibility forever” seems like unnecessary shackles, and the language should be able to grow — I’ve seen some nice proposals for improvements. I just wonder what the strategy is going to be for migrating code from go1 to go2 and how painful that’s going to be.

lolive|4 years ago

Not a Go développer here, but let’s take the example of Java when it got (for example) generics or functional features. There was NO going back. These were so fundamental improvements that the past was absolutely outdated as soon as those new features were available. May be the same thing will happen for Go

mseepgood|4 years ago

> Serious question: what are the odds that go 2

The Go maintainers already said that they don't have any plans to do an actual version 2.0 anymore. Generics turned out to be possible without breaking backward-compatibility.

asah|4 years ago

IMHO the long python3 migration was well executed and we're now comfortably on the back side of it. Reminds me of perl4=>5 and other big lifts.

Yes, commercial codebases understaffed for maintenance are kinda stuck, just like any legacy system. IMHO the solution must come from the business model down. Also, security, compliance & cost can help drive priority.

avg_dev|4 years ago

It would be nice if there was a tool that re-wrote your Go-1 code in Go-2.

brnt|4 years ago

Not so serious question: what is Go2 and Python4 would be the same thing?

mariusor|4 years ago

To expand on the logging improvements, I would like to see a `context.WithLogger` context function, to allow cross package passing of a common logger instance.

hpx7|4 years ago

> Go's templating packages should support compile time type checking.

Does anyone know of a decent type safe templating package out there (for any language)?

Strom|4 years ago

React with TypeScript. Very popular and unlike Go's templates has proper type checking.

Seb-C|4 years ago

My biggest annoyance so far is the inability (and inconsistency) to have a one-liner to get a pointer to something else than a struct.

I can write x = &Foo{...} but somehow x = &42 and x = &foo() are not allowed, which forces me in some cases to declare useless variables that hurts readability.

rank0|4 years ago

I think Golang is awesome, but I have two major gripes that I hope can be fixed:

Dependency management:

Go mods is a dumpster fire. `go get` and `go install` is finicky and inconsistent across systems.

It's difficult to import local code as a dependency. Using mods with replace feels like a shitty hack, and requires me to maintain a public repo for something I may not want to be public. I end up using ANOTHER hack that replaces mod references to private repos and I have to mess with my git config to properly authenticate.

I've never used another language that made it so difficult to import local code. Rust's cargo is so much easier to use!

Sane dynamic json parsing:

Having to create a perfectly specified struct for every single json object I need to touch is terrible UX. Using `map[string]interface{}` is just gross.

Again, I think Go should copy the existing rust solution from serde. With serde, I define the struct I need, and when I parse an object, the extra fields just get thrown out.

If anyone thinks I'm misunderstanding something, please enlighten me. I hope reasonable solutions already exist and I just haven't found them yet.

tyree731|4 years ago

> It's difficult to import local code as a dependency. Using mods with replace feels like a shitty hack, and requires me to maintain a public repo for something I may not want to be public. I end up using ANOTHER hack that replaces mod references to private repos and I have to mess with my git config to properly authenticate.

With regards to this concern at least go 1.18 is adding workspaces, which should help (https://sebastian-holstein.de/post/2021-11-08-go-1.18-featur...).

CamouflagedKiwi|4 years ago

What you describe for JSON is already the case in Go; the stdlib json parser does simply throw out any extra fields on deserialisation.

rowanseymour|4 years ago

Hardly important but I hope that in parts of the std lib where they added support for contexts by adding a `FooContext` func for every `Foo` and latter just calls the former with `context.Background()`.. we can just have `Foo` that takes a context argument.

da39a3ee|4 years ago

Can someone explain this one? I couldn't see why go gave this result.

> What is the value of cp? If you said [A B C], sadly you are incorrect. The value of cp is actually: [C C C]

masklinn|4 years ago

The "trap" of the snippet is that `cp` is an array of pointers.

What it shows is that Go doesn't have a `value` per iteration, it has a single `value` for the entire loop which it updates for each iteration. This means if you store a pointer to that, you're going to store a pointer to the loop variable which gets updated, and thus at the end of the loop you'll have stored a bunch of pointers to the last item.

This is most commonly an issue when creating a closure inside a loop, as the closure closes over the binding, and since Go has a single binding for the entire loop all the closures will get the same value.

makapuf|4 years ago

in the loop, you set cp[i] to a reference to the variable value. value is the same variable through the loop, with different values copied inside it, first A then B then C. So at the end you have cp having three times a reference to value, with the last value in it, namely C.

synergy20|4 years ago

Most important items in my plate

    1. a unified improved *error* in stdlib with stack trace support.
    2. a unified log interface(mentioned)
    3. a STL library like c++
    4. shared library support so we dont have 100 static binaries that among them each have 90% of duplicated content. go shall support shared libraries/modules officially.

pipeline_peak|4 years ago

I’ve heard Go program’s execution performance is near Java.

Is this true because I can’t think of anything more useless than that.

itgkbcdfhb|4 years ago

I want golang to stay as minimal as possible. I think of go as 2020s version of C. If you want all the madness of templating, reflection and (arguably) needless features can some privileged PhD student out there please make 2020s C++… go++?

vasergen|4 years ago

Not go developer, just curious about the language and ecosystem and really like it. For me would be nice to have more functional features. For example - explicitly say that a variable is mutable / immutable, preferably have immutability by default. Also native support for map/filter/reduce/etc. Those are good abstraction and it is easier to read than `for loops`, since you don't need to look over shoulders all the time. Guess latest would be easier to add since there is support for generics already.

jerf|4 years ago

"Also native support for map/filter/reduce/etc."

Native support for them in the context of the existing Go spec will be coming with the next release. To reserve the right to evolve the native support before committing it to the backwards compatibility promise, it will initially appear in the https://pkg.go.dev/golang.org/x/exp external repository, but that is the official Go repo for things either too unstable to be included in the standard library, or still experimental. General expectation is it'll be in the standard library in the release after that. It won't be in the standard library, but it's as official as it can be beyond that.

I carefully phrased that with "in the context of the existing Go spec", because I think expectations of this support are wildly out of whack with the reality. It's still going to be a very unpleasant style to work in, with many and manifold problems: http://www.jerf.org/iri/post/2955 . I think people will be crazy to turn to that style in Go. Go wasn't just missing generics to support this style, it was missing many things, and "solving" the generics problem still leaves it missing many things.

tgv|4 years ago

What would you need those for? For checking exhaustive type switches? Seems like an extra keyword on "interface" would do the trick.

exdsq|4 years ago

Is this comment meant for this article? I can't for the life of me grok what you mean!