Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: spec: type inferred composite literals #12854

Open
neild opened this issue Oct 6, 2015 · 151 comments
Open

proposal: spec: type inferred composite literals #12854

neild opened this issue Oct 6, 2015 · 151 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@neild
Copy link
Contributor

neild commented Oct 6, 2015

Composite literals construct values for structs, arrays, slices, and maps. They consist of a type followed by a brace-bound list of elements. e.g.,

x := []string{"a", "b", "c"}

I propose adding untyped composite literals, which omit the type. Untyped composite literals are assignable to any composite type. They do not have a default type, and it is an error to use one as the right-hand-side of an assignment where the left-hand-side does not have an explicit type specified.

var x []string = {"a", "b", "c"}
var m map[string]int = {"a": 1}

type T struct {
  V int
}
var s []*T = {{0}, {1}, {2}}

a := {1, 2, 3} // error: left-hand-type has no type specified

Go already allows the elision of the type of a composite literal under certain circumstances. This proposal extends that permission to all occasions when the literal type can be derived.

This proposal allows more concise code. Succinctness is a double-edged sword; it may increase or decrease clarity. I believe that the benefits in well-written code outweigh the harm in poorly-written code. We cannot prevent poor programmers from producing unclear code, and should not hamper good programmers in an attempt to do so.

This proposal may slightly simplify the language by removing the rules on when composite literal types may be elided.

Examples

Functions with large parameter lists are frequently written to take a single struct parameter instead. Untyped composite literals allow this pattern without introducing a single-purpose type or repetition.

// Without untyped composite literals...
type FooArgs struct {
  A, B, C int
}
func Foo(args FooArgs) { ... }
Foo(FooArgs{A: 1, B: 2, C:3})

// ...or with.
func Foo(args struct {
  A, B, C int
}) { ... }
Foo({A: 1, B: 2, C: 3})

In general, untyped composite literals can serve as lightweight tuples in a variety of situations:

ch := make(chan struct{
  value string
  err   error
})
ch <- {value: "result"}

They also simplify code that returns a zero-valued struct and an error:

return time.Time{}, err
return {}, err // untyped composite literal

Code working with protocol buffers frequently constructs large, deeply nested composite literal values. These values frequently have types with long names dictated by the protobuf compiler. Eliding types will make code of this nature easier to write (and, arguably, read).

p.Options = append(p.Options, &foopb.Foo_FrotzOptions_Option{...}
p.Options = append(p.Options, {...}) // untyped composite literal
@adg adg added the Proposal label Oct 6, 2015
@adg
Copy link
Contributor

adg commented Oct 6, 2015

There is some prior art. We actually implemented this (or something very similar) in the lead-up to Go 1.

The spec changes:

https://codereview.appspot.com/5450067/
https://codereview.appspot.com/5449067/

The code changes:

https://codereview.appspot.com/5449071/
https://codereview.appspot.com/5449070/
https://codereview.appspot.com/5448089/
https://codereview.appspot.com/5448088/

There may be other changes that I'm missing. But in the end we abandoned the changes (and reverted the spec changes); it tended to make the code less readable on balance.

@bcmills
Copy link
Contributor

bcmills commented Oct 6, 2015

I only see one spec change there (the other one you linked is the compiler implementation).

At any rate: "tend[ing] to make less readable on balance" depends a lot on the specific code. Presumably we've learned more about real-world Go usage (including Protocol Buffers and a variety of other nesting data-types) in the time since then - perhaps it's worth revisiting?

(I've badly wanted literals for return values and channel sends on many occasions - they would be particularly useful when a struct is just a named version of "a pair of X and Y" and the field names suffice to fully describe it.)

@minux
Copy link
Member

minux commented Oct 6, 2015 via email

@neild
Copy link
Contributor Author

neild commented Oct 6, 2015

Under this proposal, the following assignments are identical:

var m map[string]int
m = map[string]int{A: 1}
m = {A: 1}

The only difference is that in the latter case, the type of the literal is derived from the RHS of the expression. In both cases, the compiler will interpret A as a variable name.

I would not allow const untyped composite literals; that's a can of worms.

I think untyped composite literals would be too confusing to use (and compile!) if they came with a default type. :)

@neild
Copy link
Contributor Author

neild commented Oct 6, 2015

On readability:

This would be a significant language change. Go code would become somewhat terser, on balance. In some cases this would lead to less readable code; in others more so.

I feel that the benefits would outweigh the costs, but that's obviously a subjective judgement. In particular, I think that lightweight tuples (as in the above examples) would be a substantial improvement in a number of places.

@jimmyfrasche
Copy link
Member

I assume that this proposal is simply expanding the places in which you can elide a type literal.

If so, referring to it as untyped composite literals is a bit confusing as untyped has a specific meaning in Go.

It might make more sense to consider each place separately. I don't see much of a point, other than consistency, in allowing

var t T = {}

since you could just do

var t = T{}

But the rest would certainly cut down on typing and allow nicer APIs in places.

For example,

Polygon({1, 2}, {3, 4}, {5, 4})

is arguably clearer than

Polygon([]image.Point{{1, 2}, {3, 4}, {5, 4}})

and the only alternative at present would be

Polygon(image.Point{1, 2}, image.Point{3, 4}, image.Point{5, 4})

@adg
Copy link
Contributor

adg commented Oct 6, 2015

I agree that the examples look nice, at a glance. But anything can look
nice without context.

To move this proposal forward, one should apply the change to a corpus of
real Go code so that we may observe its benefits and drawbacks in context.

On 7 October 2015 at 09:12, Damien Neil [email protected] wrote:

On readability:

This would be a significant language change. Go code would become somewhat
terser, on balance. In some cases this would lead to less readable code; in
others more so.

I feel that the benefits would outweigh the costs, but that's obviously a
subjective judgement. In particular, I think that lightweight tuples (as in
the above examples) would be a substantial improvement in a number of
places.


Reply to this email directly or view it on GitHub
#12854 (comment).

@minux
Copy link
Member

minux commented Oct 6, 2015 via email

@neild
Copy link
Contributor Author

neild commented Oct 6, 2015

To be clear, this proposal does not allow {A: 1} to implicitly become map[string]int{"A":1}.

@minux
Copy link
Member

minux commented Oct 6, 2015 via email

@bcmills
Copy link
Contributor

bcmills commented Oct 6, 2015

In that last example, A is an identifier (i.e. for a string variable or constant) - not a string literal itself.

@minux
Copy link
Member

minux commented Oct 6, 2015

did you mean that the code is actually:

const A = "A"
var m map[string]int
m = {A: 1}

Then there are more ambiguity in syntax.
const A = "A"
var x struct { A int }
x = {A: 1}

What does this mean?

Note my concern is that it's possible to assign {A:1} to
vastly different types: map[string]int and struct { A int }
(what about map[interface{}]int and map[struct{string}]int?)

@neild
Copy link
Contributor Author

neild commented Oct 6, 2015

x = {A: 1} is precisely equivalent to x = T{A: 1}, where T is the type of x.

@minux
Copy link
Member

minux commented Oct 6, 2015 via email

@neild
Copy link
Contributor Author

neild commented Oct 6, 2015

We do, actually:
http://play.golang.org/p/YubepmdVwy

A := "A"
var m map[string]int
m = map[string]int{A: 1}
fmt.Println(m)

@minux
Copy link
Member

minux commented Oct 6, 2015 via email

@neild
Copy link
Contributor Author

neild commented Oct 6, 2015

If A is not otherwise defined, then the first case (m = {A: 1}) will fail to compile with the same error you would get if it were written m = map[string]int{A: 1}. i.e., it is syntactically valid but incorrect because A is undefined.

@griesemer
Copy link
Contributor

@minux The implementation of this proposal is actually rather straight-forward and the explanation reasonably simple and clear: Whenever the type of a composite literal is known, we can elide it. Once the type is known, the meaning of a composite literal key value ('A' in the previous examples) is answered the same as it is before.

(Implementation-wise this only affects the type-checker, and there's already provisions for this for the case where we allow type elision already.)

So the proposal is indeed simply a relaxation of the existing rules as @jimmyfrasche pointed out.

Another way of phrasing this is: In an assignment in all its forms (including parameter passing, "assigning" return values via a return statement, setting values in other composite literals, channel sends, there may be more) where the type of the destination is known, we can leave away the composite literal type if the value to be assigned/passed/returned/sent is a composite literal.

(We cannot leave it away in a variable declaration with an initialization expression where the variable is not explicitly typed.)

In the past we have deliberately restricted this freedom even within composite literals. This liberal form would overturn that decision.

Thus, we may want to start more cautiously. Here's a reduced form of the proposal that enumerates all permitted uses:

In addition to the existing elision rules inside composite literals, we can also elide the composite literal type of a composite value x when

  1. x is assigned to a lhs (if x is an initialization expression, the lhs must have an explicit type)
  2. x is passed as an argument
  3. x is returned via a return statement
  4. x is sent to a channel

In all cases, the variable/parameter/return value/channel value type must be a composite literal type (no interfaces).

We could even reduce further and start with only 1) or 2).

@bcmills
Copy link
Contributor

bcmills commented Oct 9, 2015

The downside of limiting the cases in which elision is allowed is that the programmer must remember what those cases are. The two extreme endpoints ("never" and "whenever the type is otherwise known") are easier to remember - and simpler to describe - due to their uniformity.

@neild
Copy link
Contributor Author

neild commented Oct 9, 2015

On Tue, Oct 6, 2015 at 3:17 PM, Andrew Gerrand [email protected]
wrote:

To move this proposal forward, one should apply the change to a corpus of
real Go code so that we may observe its benefits and drawbacks in context.

I agree that it would be good to apply this change to a corpus of real code
to observe its effects. I'm hunting through the stdlib to see if I can find
a package that might change in an interesting fashion. Simply eliding all
types in composite literals is uninformative, since the more interesting
uses (e.g., lightweight tuples as function parameters) require some light
refactoring.

@griesemer
Copy link
Contributor

@bcmills I would agree if we started with this as a general concept. In this case "whenever the type is otherwise known" is not sufficiently clear. For instance, in a struct comparison a == b, the types may be known but the exact rules are subtle.

This proposal is a rule that describes an exception, namely when it is allowed to elide a composite literal type. It is clearer to be explicit.

@bcmills
Copy link
Contributor

bcmills commented Oct 9, 2015

This proposal is a rule that describes an exception, namely when it is allowed to elide a composite literal type. It is clearer to be explicit.

That assumes that "eliding the type" is the exception rather than the rule. s/allowed to elide/necessary to specify/ and the same argument applies in the other direction.

(We can explicitly enumerate the cases in which a type tag is necessary in exactly the same way that we can explicitly enumerate the cases in which it is not.)

@jimmyfrasche
Copy link
Member

The only other case I can think of (for consideration, if not inclusion) is eliding the type within another composite literal like

 pkgname.Struct{
   Field: {...},
 }

for

 pkgname.Struct{
   Field: pkgname.AnotherCompositeLiteral{...},
 }

@minux
Copy link
Member

minux commented Oct 10, 2015 via email

@ianlancetaylor
Copy link
Contributor

@minux This is a type elision proposal. The term "untyped" in the title is misleading.

I can't tell: is there anything specific you object to this in this proposal?

(I'm not sure I support this proposal myself, but I'm trying to understand your objection.)

@codeblooded
Copy link

This is a turn of events. I initially proposed #12296, but I found that named parameters where not a solution with current Go-idioms.

As for inferred structs… I have been in favor of this for a while; however, I have recently hit some pitfalls. I'm (now, surprisingly) leaning against this, because of legibility and common behavior:

// assume there are 2 struct types with an exported Name of type string
// they are called Account and Profile…

// Would this result in an Account of a Profile?
what := {Name: "John"}

(see http://play.golang.org/p/KM5slOe7nZ)

Perhaps, I'm missing something but duck typing does not apply to similar structs in the language…

type Male struct {
    IsAlive bool
}

type Female struct {
    IsAlive bool
}

Even though Male and Female both only have IsAlive, a Male ≠ a Female.

@bcmills
Copy link
Contributor

bcmills commented Oct 14, 2015

what := {Name: "John"}

would produce a compile error under this proposal.

(It fails the "when the literal type can be derived" constraint, which would be applied per-statement. For this statement, the elided type cannot be derived unambiguously: it could by any struct type with a "Name" field, including an anonymous struct type. If there is a constant or variable named "Name" in scope, it could be a map type as well.)

@bcmills
Copy link
Contributor

bcmills commented Oct 14, 2015

You would, however, be able to do the equivalent for plain assignments, as long as the variable has a concrete type:

var acc Account
acc = {Name: "Bob"}  // ok: we already know that acc is an Account struct.

var profile interface{}
profile = {Name: "Bob"}  // compile error: profile does not have a concrete type.

@codeblooded
Copy link

@bcmills Ok… what would be the overhead on the compiler side of inferring the types and aborting if the type is ambiguous?

@rprtr258
Copy link

This would make slog's API nicer. (See #56345.) Instead of

slog.Info("msg", "a", "b", "c", "d")

which many people don't like because it's hard to tell what are keys and what are values, we could instead write

slog.Info("msg", {"a", "b"}, {"c", "d"})

which is marginally more verbose but much easier to read.

That is not the case for the proposal. slog.Info accepts (string, ...any), so no way to recognize the type of {"a", "b"}.

And, even if we were to restrict slog.Info to signature like (string, ...slog.Attr), slog.Attr as of now is

struct {
	Key   string
	Value slog.Value
}

So, if we elide {"a", "b"} to slog.Attr, "a" becomes Key and "b" should become Value. But slog.Value is somewhat complex struct with no public fields, so still no way to elide slog.Info arguments.

Now, even if we suppose slog.Value to be just any type, {"a", "b"} still could not be elided, since there are two possibilities:

  • Value is string("b"), corresponding to slog.String("a", "b") for {"a", "b"}
  • Value is any("b"), corresponding to slog.Any("a", "b") for {"a", "b"}

and {"a", "b"} still could not be elided.

which is similar to argument made in #12854 (comment)

@natefinch
Copy link
Contributor

This is an ongoing difficulty for me. I tend to make a lot of "config files" in regular go code, because why map from yaml or whatever to go, when I can just write it in go and get compile time checks, code completion, etc.

However, there are a lot of cases where type inference should let me elide the types and yet I can't, leading to less readable code.

For example:

type User struct {
    Email string
}
type Group struct {
    Leader User
}

var Groups map[string]Group = {
    "devs" : {
        Leader: User{
            Email: "[email protected]",
        }
    }
}

The go tool was modified so maps and slices don't need to specify the type when it's a struct, but struct fields still need to have the type specified when the field is a struct. Why? If the field name is obvious in context, you really don't need the type name.

This seems perfectly legible to me:

var Groups map[string]Group = {
    "devs" : {
        Leader: {
            Email: "[email protected]",
        }
    }
}

And certainly, the lack of User after Leader is no less expressive than the lack of Group after "devs". You don't need to see the Group type to see that the Leader has an email of "[email protected]". If that's all you're writing anyway, adding User is not really adding any information.

This gets compounded with larger structs, especially if you're making an anonymous struct as a field in another struct.

type Config struct {
    Telemetry struct {
       Port int
       URL string
       Password string
    }
    DB struct {
       ConnectionStr string
       Password string
    }
}

I like making anonymous structs as fields inside another struct because the whole of the struct definition is all in one place. You don't have to jump around to see what each field's sub-fields are. It uses the anonymous structs as namespacing, with short, clear field names in the anonymous structs. There's very clearly no logic tied to the struct fields, because they're not defined as individual types. It's just data representation.

The problem with anonymous struct fields right now is that constructing that type requires re-specifying the whole structure of the anonymous struct, even though the compiler already knows what it has to be.

cfg := Config {
    Telemetry: struct {Port int; URL string; Password string} {
        Port: 8888,
        URL: ProdTelemUrl,
        Password: os.Getenv("TELEMETRY_PASSWORD"),
    },
    DB: struct {ConnectionStr string; Password string} {
        ConnectionStr: ProdDBUrl,
        Password: os.Getenv("DB_PASSWORD"),
    },
}

If the go tool could elide the struct definitions this could be:

cfg := Config {
    Telemetry: {
        Port: 8888,
        URL: ProdTelemUrl,
        Password: os.Getenv("TELEMETRY_PASSWORD"),
    },
    DB: {
        ConnectionStr: ProdDBUrl,
        Password: os.Getenv("DB_PASSWORD"),
    },
}

This is both easier to read and easier to write, and matches up really nicely with the struct definition.

To avoid the verbose retyping of anonymous struct types, people either make separate types, which make you jump around just to see what all the fields for this config are, and make you wonder if there's actual logic on that TelemetryConfig struct, or they put all the fields in the base struct and prefix the names with their namespace, which is repetitive and harder to read because of all the long field names.

Please update the go tool to allow us to elide types when constructing struct literals with fields that are also structs. It would make Go almost as concise as yaml, but without all the yaml footguns.

@ianlancetaylor @rsc

@ydnar
Copy link

ydnar commented Aug 21, 2023

It uses the anonymous structs as namespacing, with short, clear field names in the anonymous structs.

It would be nice to be able to grab an anonymous struct type out of the parent:

type Config struct {
    Telemetry struct {
       Port int
       URL string
       Password string
    }
    DB struct {
       ConnectionStr string
       Password string
    }
}

var db Config.DB

// do something with db

@bronger
Copy link

bronger commented Aug 21, 2023

@natefinch I really think this is in the eye of the beholder, whether the struct type info is helping or not. FWIW, it makes your first example more legible to me if there is an explicit User. I remember a couple of times when I was annoyed to be forced to spell out type information in similar situations, but it was my code and I knew it well.

@DeedleFake
Copy link

@ydnar

It's not really what it was designed for, but some of the proposed solutions in #34515 could theoretically allow that, such as var db typeof(Config.DB).

@ydnar
Copy link

ydnar commented Aug 22, 2023

It's not really what it was designed for, but some of the proposed solutions in #34515 could theoretically allow that, such as var db typeof(Config.DB).

Inspired by Swift’s use of empty enums as namespaces, which can allow for less-mangled type names. I don’t want to hijack this proposal and will discuss elsewhere.

@natefinch
Copy link
Contributor

Here's a really good comparison. Almost identical definitions, but the struct form doesn't let you elide the type:

type User struct {
	Email string
}
type GroupMap map[string]User

type GroupStruct struct {
	Leader User
}

func main() {
	_ = GroupMap{
		"Leader": {
			Email: "[email protected]",
		},
	}

	_ = GroupStruct{
		Leader: User{
			Email: "[email protected]",
		},
	}
}

@mikeschinkel
Copy link

mikeschinkel commented Jan 30, 2024

@VinGarcia — Your summarizing comment from almost 3 years ago did not include as the reason for eliding probably my number 1 feature request for Go, and that is for use when passing options via an options struct:

package main

type StandardWidgetOptions struct {
	Foo int
	Bar string
	Baz bool
}

type StandardWidget struct {
	Name string
	Opts StandardWidgetOptions
}

func NewStandardWidget(name string, opts StandardWidgetOptions) *StandardWidget {
	return &StandardWidget{
		Name: name,
		Opts: opts,
	}
}

func main() {
	// Current malaise
	w1 := NewStandardWidget("my_widget1", StandardWidgetOptions{
		Foo: 10,
		Bar: "Hello",
		Baz: false,
	})

	// Hoped for future
	w2 := NewStandardWidget("my_widget2", {
		Foo: 25,
		Bar: "Sweet", 
		Baz: true,
	}) 
	
	// Or, also!
	w3 := NewStandardWidget("my_widget2", {Foo: 100,Bar: "World",Baz: true})

	// Stop Go complaining about unused vars
	print(w1,w2,w3)
}

Just commenting in hopes to keep the dream alive... 🙌

@humdogm
Copy link

humdogm commented Feb 20, 2024

I read through this thread, and I believe I have a different use case.

I want to loop over a slice of test cases. I also want to keep the test and expected values close together.

With current syntax, it looks something like:

type testData struct {
	test     []int
	expected []int
}

var tests = []testData{
	{[]int{0, 0}, []int{0, 0}},
	{[]int{0, 1}, []int{1, 0}},
}

I have 18 such test cases in this slice. In my opinion, all of the []ints that I would have to write are visual clutter when they can be elided from the struct definition.

If I want to use this style, the below is also valid syntax:

type testData [2][]int

var tests = []testData{
	{{0, 0}, {0, 0}},
	{{0, 1}, {1, 0}},
}

and in this case the types are elided, but with this, I lose the ergonomics from the gopls language server showing me the field name hints that clearly label each test case's test data and expected values.

@ngrilly
Copy link

ngrilly commented Feb 20, 2024

I agree with @mikeschinkel and @humdogm. Zig is solving that problem with Anonymous Struct Literals. They allow omitting the struct type of the literal, and can be coerced to an actual struct type if the coercion is completely unambiguous.

@eihigh
Copy link

eihigh commented Mar 21, 2024

Currently, the functional option pattern is often introduced as a way to achieve optional arguments in Go. However, I feel this approach is not appropriate because it introduces excessive complexity. If zero values were sufficient, we could simply use structs instead of functional option patterns (FOPs), but this is not widely recognized at the moment.

If users are frustrated by having to write long struct names for every call and think, "Go has no way to achieve optional arguments except through FOP!" then I believe the introduction of this feature could lead many programmers to make better design choices.

I also believe that being able to write shorter code, not only for FOP but also for functions that take tuples, such as in logging with log/slog, could lead to better designs as well.

@eihigh
Copy link

eihigh commented Mar 24, 2024

I am in favor of this proposal and will try to answer the concerns that have been raised.

The key of map initialization and the field name of the structure are indistinguishable

This is probably the most critical issue, in my opinion, as only structs can be untyped, or map and slice would need to have a separate syntax. I do not consider it that important to be able to write a map or slice that way, compared to being able to shortly define structures that have important uses, such as tuples and optional arguments.

m := map{keyStr: "value"}
s := []{idxInt: "value"} // []string
v := {value: "value"} // struct{value string}

Implicitness has a negative impact on readability

I don't think this is a big problem; Go already has a lot of "if you don't know the definition, you don't know the type" situations.

x := f() // what's the type of x?
ch <- {value: "value"} // what's the type of ch?

@DeedleFake
Copy link

The key of map initialization and the field name of the structure are indistinguishable

This is probably the most critical issue, in my opinion, as only structs can be untyped, or map and slice would need to have a separate syntax. I do not consider it that important to be able to write a map or slice that way, compared to being able to shortly define structures that have important uses, such as tuples and optional arguments.

m := map{keyStr: "value"}
s := []{idxInt: "value"} // []string
v := {value: "value"} // struct{value string}

Unless I'm misunderstanding something, that syntax overlap doesn't exist. There's no unquoted shortcut syntax for map keys currently.

@cespare
Copy link
Contributor

cespare commented Mar 25, 2024

@DeedleFake it's not "unquoted keys", it's the fact that the key can be a variable: https://go.dev/play/p/TvEURZjvfHR

@neild
Copy link
Contributor Author

neild commented Mar 25, 2024

It's already the case that you can't tell what the key of a composite literal is at parse time.

f(T{
  Key: "value", // Is Key a value, or a field name? We don't know until we know what T is.
})

Eliding T from the composite literal doesn't add any additional parsing ambiguity. We already need to defer interpretation until typecheck.

@dtrckd
Copy link

dtrckd commented Jun 2, 2024

Hi,
I'm sorry to jump into the conversation like a hairsbreadth, but having just discovered this language feature, I investigated to find out when and it which release it was introduced. But I am struggling to find out, and feel confused about the fact that this issue is still open while the type inference in composite literal seems to be implemented in go1.22.

I'd be grateful to any soul who could enlighten me on this matter.

ps: I am referring to this part of specification in particular : Within a composite literal of array, slice, or map type T, elements or map keys that are themselves composite literals may elide the respective literal type if it is identical to the element or key type of T. Similarly, elements or keys that are addresses of composite literals may elide the &T when the element or key type is *T.

@neild
Copy link
Contributor Author

neild commented Jun 2, 2024

This proposal is neither accepted nor implemented. The part of the specification you're referencing is what this proposal proposes to amend.

@igorcafe
Copy link

igorcafe commented Jun 2, 2024

That would be specially useful for nested structs like for table-driven tests, json DTOs, and so on:

tests := []struct{
  desc string
  val1 int
  val2 bool
  val3 string
  user struct{
    id string
    name string
    address string
  }
}{
  {
    desc: "test name",
    val1: 123,
    user: {
      id: "user id",
      name: "name",
    },
  },
  {
    desc: "test name",
    val1: 123,
    user: {
      id: "user id",
      name: "name",
    },
  },
  {
    desc: "test name",
    val1: 123,
    user: {
      id: "user id",
      name: "name",
    },
  },
}

instead of:

tests := []struct{
  desc string
  val1 int
  val2 bool
  val3 string
  user struct{
    id string
    name string
    address string
  }
}{
  {
    desc: "test name",
    val1: 123,
    user: struct{
      id string
      name string
      address string
    }{
      id: "user id",
      name: "name",
    },
  },
  {
    desc: "test name",
    val1: 123,
    user: struct{
      id string
      name string
      address string
    }{
      id: "user id",
      name: "name",
    },
  },
  {
    desc: "test name",
    val1: 123,
    user: struct{
      id string
      name string
      address string
    }{
      id: "user id",
      name: "name",
    },
  },
}

@antichris
Copy link

antichris commented Jun 16, 2024

instead of:

(code snippet elided)

Or, as a sensible gopher would write,

type testUser struct{
  id string
  name string
  address string
}
tests := []struct{
  desc string
  val1 int
  val2 bool
  val3 string
  user testUser
}{
  {
    desc: "test name",
    val1: 123,
    user: testUser{
      id: "user id",
      name: "name",
    },
  },
  {
    desc: "test name",
    val1: 123,
    user: testUser{
      id: "user id",
      name: "name",
    },
  },
  {
    desc: "test name",
    val1: 123,
    user: testUser{
      id: "user id",
      name: "name",
    },
  },
}

Which, in this very specific case, is not really that awful.

Overall, yeah, as it stands now, even though you get a concise, hopefully, clear name you can look up for more details about it, you also have to pollute the scope where the type of (in this case) tests gets defined with a stupid named helper type you have little to none other use. Which is not optimal.

@igorcafe, you're making a very valid point.

But it has already been made before, and, sadly, does not contribute much more new information to the discussion here than a simple 👍 on the relevant prior posts. Be sure you haven't missed other ideas relevant to you in earlier posts! 😉

@igorcafe
Copy link

@antichris problem in that is that you need to create a new type that will only be used as a "nested type", which adds unnecessary complexity to the code, when the real relevant type is the top level type

@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change labels Aug 6, 2024
@jimmyfrasche
Copy link
Member

Similar to the last few posts, one of the major places where this has been pinching me lately is table tests with subtables. I'll have tests that are identical except for some shared config that would be awkward to manage if duplicated in dozens of consecutive rows or as separate tests that are identical except for some configuration. What I'd really like to do is just:

var table = []struct{
  /* shared config */
  subtable []struct {
    /* input/expected */
  }
}{
  {/*...*/, {
    {/*...*/},
  }},
}

(There are many, many ways to write that now but they all involve adding extraneous bits that at best don't add anything to the code and at worst litter it with distractions.)

@Galabar001
Copy link

Similar to the last few posts, one of the major places where this has been pinching me lately is table tests with subtables. I'll have tests that are identical except for some shared config that would be awkward to manage if duplicated in dozens of consecutive rows or as separate tests that are identical except for some configuration. What I'd really like to do is just:

var table = []struct{
  /* shared config */
  subtable []struct {
    /* input/expected */
  }
}{
  {/*...*/, {
    {/*...*/},
  }},
}

(There are many, many ways to write that now but they all involve adding extraneous bits that at best don't add anything to the code and at worst litter it with distractions.)

Just another vote for this in table drive tests. While working with Go, this has always been painful and the proposal would make this much nicer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests