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: Go 2: add C# like nameof() built-in function #37039

Closed
beoran opened this issue Feb 5, 2020 · 24 comments
Closed

Proposal: Go 2: add C# like nameof() built-in function #37039

beoran opened this issue Feb 5, 2020 · 24 comments
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@beoran
Copy link

beoran commented Feb 5, 2020

Proposal

I propose to add C# like nameof built-in compile time function. This proposal is based on the C# standard here: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/nameof. The main use of this is to make error values and logging easier to refactor, especially when using tools such as go pls, and also for use in ORM and intepreters where the name of an identifier, constant or stuct member, might often have to be mapped to an identical string value.

nameof built-in function

The nameof built-in function obtains the name of a variable, type, or member
as a string constant:

// In the shopping package, we have the following type: 

type CartItem struct {
    Name string
    Stock int
    Price uint
    profitMargin float64
}

// And suppose we have this method on it.
func (c * CartItem) Checkout() {
}

// Then in another package: 
fmt.Println(nameof(shopping.CartItem))  // output: CartItem
fmt.Println(nameof(int))  // output: int
fmt.Println(nameof(shopping.CartItem.Price))  // output: Price
fmt.Println(nameof(shopping.CartItem.Checkout))  // output: Checkout
ci := shopping.CartItem{}
fmt.Println(nameof(ci.Checkout))  // output: Checkout

// more examples
var numbers = []int{ 1, 2, 3 };
fmt.Println(nameof(numbers));  // output: numbers
fmt.Println(nameof(len(numbers)))  // output: len
fmt.Println(nameof(nameof));  // output: nameof

// Expressions which have no name return the empty string.
fmt.Println(nameof())  // output: (the empty string)
fmt.Println(nameof(1+2))  // output: (the empty string)

// usage with constants
type AstKind string
const AstExpression = AstKind(nameof(AstExpression))
fmt.Println(AstExpression)  // output: AstExpression

// However, names that don't exist or are not exported are a compilation error
fmt.Println(nameof(CartItem.Reductions))  // error: nameof(): CartItem has no field Reductions 
fmt.Println(nameof(CartItem.profitMargin)) // error: nameof(): CartItem field profitMargin is not exported
fmt.Println(nameof(someUndefinedFoo)) // error: nameof(): Unknown variable someUndefinedFoo 

As the preceding example shows, in the case of a type and a package, the produced name is not fully qualified. The same goes for struct members, only the name of the last field in the chain of field accesses is produced.

The nameof built-in function is evaluated at compile time and has no effect at run time.

You can use the nameof built-in function or ORM, for constants that evaluate to themselves as a string value, and for debugging, or to make error values or log messages more maintainable. For example:

import "github.com/sirupsen/logrus"

type Named {
    Name *string
}

func (n *Named) SetName(name *string) error {
    if name == nil {
        logrus.WithField(nameof(name), name).Error("cannot be nil")
        return fmt.Errorf(nameof(name) + " cannot be nil" )
    }
    n.Name = name
}

Proposal template

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    I am experienced, I have been using Go for 10 years since before v1.0.
  • What other languages do you have experience with?
    Batch files, Basic, C, C++, D, Java, Javascript, Lua, Objective-C, Oberon, Pascal, PHP, Python, R, Ruby, Shell scripts, Visual Basic, ... I do not have experience with C#, but in the previous issues, it seemed that a C#-like nameof was the one most people seemed interested in for Go as well.
  • Would this change make Go easier or harder to learn, and why?
    It would not make Go easier to learn, but also not substantially harder. If the new built-in function is well documented, everyone who doesn't know it yet can read its documentation to find out how it works. Furthermore, it will be familiar to C# users.
  • Has this idea, or one like it, been proposed before?
    This idea is based on the proposal proposal: Go 2: obtain struct field name in type-safe way #36332 and proposal: Go 2: add nameof() built-in function #36924.
  • If so, how does this proposal differ?
    In stead of special syntax I proposed a built-in function, which is more Go-like and easier to learn. Furthermore, it is not based on reflection, but a compile time,, similar as it is in C# which has proven utility in that language, and it seems like most people are interested in this kind of nameof() function.
  • Who does this proposal help, and why?
    This proposal would help people writing ORM's, interpreters, or with debugging aid, and other people who need to use the names of Go language artifacts, such as struct field names, or type names have to be used as strings. It is especially convenient to prevent errors during refactoring when using tools such as go pls.
  • Is this change backward compatible?
    Yes, adding a new built in function is normally completely backwards compatible, and does not break the Go1 compatibility guarantee.
  • Show example code before and after the change.

Example: Logging and error values:

Before:

import "github.com/sirupsen/logrus"

type Named {
    Name *string
}

func (n *Named) SetName(name *string) error {
    if name == nil {
        logrus.WithField("name", name).Error("cannot be nil")
        return fmt.Errorf("name cannot be nil" )
    }
    n.Name = name
}

After:

import "github.com/sirupsen/logrus"

type Named {
    Name *string
}

func (n *Named) SetName(name *string) error {
    if name == nil {
        logrus.WithField(nameof(name), name).Error("cannot be nil")
        return fmt.Errorf(nameof(name) + " cannot be nil" )
    }
    n.Name = name
}

While the before looks simpler, it has a problem: when you rename the namevariable, especially using tools, "name" in the string will not be changed by the tool as well. The nameof function can be supported by such tools, and then refactor the name correctly so the error ans log message will also be correct.

Example: Orm-ish use:

Before:

import "database/sql"

type CartItem struct {
    Name string
    Stock int
    Price uint    
}

func (ci CartItem) Insert(db *sql.DB) error {
	// Not very future proof. If the struct changes, I could easily forget to change field name here as well.
	query := "INSERT INTO CartItem (Name, Stock, Price) VALUES (?, ?, ?)"
	err := db.Exec(query, ci.Name, ci.Stock, ci.Price)
	return err
}

After:

func (ci *CartItem) Insert(db *DB) error {
	// Get the field names at compile time. Less chance for error.
        // If the fields don't exist we will get a compiler error. 
        fields := []String { nameof(ci.Name),  nameof(ci.Stock), nameof(ci.Price) } 
	query := "INSERT INTO CartItem (" + strings.Join(fields , ", ") +  ") VALUES (?, ?, ?)"
	err := db.Exec(query, ci.Name, ci.Stock, ci.Price)
	return err
}

In this case, the after code is also somewhat more complex, but it is definitely more correct. If the struct layout changes, then in stead of getting a run time error, we will get a compile time error which is far preferable in such a situation.

  • What is the cost of this proposal? (Every language change has a cost).
    The function has to be implemented, documented and tested.
    • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
      The gopls tool may or may not have to be updated to allow refactoring of the contents of a nameof() call.
    • What is the compile time cost?
      Low, because only if the function is used, the compiler has at do a few name and field lookups, but it should already have that data available.
    • What is the run time cost?
      None, the whole point is to have this functionality at compile time.
  • Can you describe a possible implementation?
    See above for the proposal based on C# documentation, extended with some more situations.
  • How would the language spec change?
    The Built-in_functions section would need to get a sub chapter on "Obtaining Names" or such.
  • Orthogonality: how does this change interact or overlap with existing features?
    It does not overlap with reflect, except maybe for names of fields, but nameof() can also fetch names of non-expressions, which is not possible using reflect. It would also allow to check on compile time whether a variable or constant exists: eg:
var _ = nameof(Foo) // compile error if Foo does not exist. May happen due to build tags, etc. 
  • Is the goal of this change a performance improvement?
    No.
  • Does this affect error handling?
    Only in that it improves the quality of error values in when renaming variables.
  • Is this about generics?
    No.
@gopherbot gopherbot added this to the Proposal milestone Feb 5, 2020
@creker
Copy link

creker commented Feb 5, 2020

// usage with constants
type AstKind string
const AstExpression = AstKind(nameof(AstExpression))
fmt.Println(AstExpression) // output: AstKind

What are the rules for that? I would expect it to output AstExpression. At least, C# works that way.

@beoran
Copy link
Author

beoran commented Feb 5, 2020

@creker: You are right. I fixed this in the proposal.

@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Feb 5, 2020
@ianlancetaylor
Copy link
Contributor

We started down this path with a suggestion for a way to get the names of the fields of a struct type as a []string. Just for my own understanding, am I correct in thinking that this proposal does not support that?

@beoran
Copy link
Author

beoran commented Feb 5, 2020

@ianlancetaylor, indeed, as we discussed in the other issue, that use case is not necessary and makes nameof() confusingly different from what is in C#.

By staying close to the C# nameof we can stick to something "tried and tested", that will be familiar to some people.

However, as you can see from the examples above, nameof() could still be used to get the names of struct fields one by one, with a compile time error if the field does not exist. That is also useful for ORM, etc.

@beoran
Copy link
Author

beoran commented Feb 6, 2020

Also, to show the utility of a nameof() construct in a programming language, consider that in the C/C++/Objective-C langauges we have the C preprocessor with the # stringify operation, which is used to good effect for many similar use cases as nameof() would be. Furthermore, a nameof() built -in function is much more clean and easy to understand than #and the C preprocessor.

@qtplatypus
Copy link

Can't this be already done via reflect?

@beoran
Copy link
Author

beoran commented Feb 7, 2020

@qtplatypus, No, the C# like nameof() in this proposal produces compile time constants, based on the names of the variables, which is not possible in the general case with reflect. Only the struct field case is covered, but this nameof proposal is much wider, as it allows to get names of anything that has a name and is in scope. Furthermore with reflect, you would get a you won't get a compile time error in case the field you are looking up does not exist.

@creker
Copy link

creker commented Feb 7, 2020

@beoran I'm not even sure it's possible to get a name of the field with reflect. I'm not a reflect expert but poking around I didn't find any obvious way that would do this

type foo struct {
    fld int
} 
fooVal := foo{}
//some reflect magic to get "fld" just from fooVal.fld at runtime

@beoran
Copy link
Author

beoran commented Feb 7, 2020

Indeed, if it is a non exported field, reflect does not work. In this case, nameof() would work fine.

@creker
Copy link

creker commented Feb 7, 2020

@beoran it doesn't even matter whether it's exported or not. I just don't see a way how can I go from struct field to its name at runtime. Only thing I can get from it with reflect is Type and Value. Both do not provide any link to the parent struct nor contain any information that it's actually a struct field.

@ianlancetaylor
Copy link
Contributor

We can see the utility of this, it would be simple to implement, and it is backward compatible. But it's not quite clear how useful this would be. It would help to point out some real existing code that would benefit from this. Are there places where the standard library would use this functionality?

In the logrus example above, the "after" code seems worse than the "before" code.

In general nameof seems only useful for names that are visible to some external operation, such as SQL or JSON. In general this seems like it might be most useful for struct field names, but not anything else (like the earlier issue).

@beoran
Copy link
Author

beoran commented Feb 19, 2020

The logrus example seems worse after, but it is in fact better because it makes it easier to refactor the variable name with tools, and to keep the string and the name of the variable matched if it changes.

Speaking for myself, the code in https://gitlab.com/beoran/muesli, an embeddable scripting language interpreter written in Go, for example in https://gitlab.com/beoran/muesli/-/blob/master/ast.go would be helped by this.

For example:
func (astkind AstMetaKindSet) String() string { return "AstSet" } would be better as func (astkind AstMetaKindSet) String() string { return nameof(AstSet) }, because I am still developing the interpreter and changing the AST kinds, so I sometimes forget to change the strings as well. With nameofI would get a compile time error in stead.

As for the Go standard library and runtime. I had a quick look and I found a few points where I think nameof could be useful:

  • I saw many tests, such as in src/runtime/pprof/pprof_test.go and other places where a function is called to test it, and the test case description contains the name of the test function as a string as well. Here, nameof() could be used to make sure that the name of the function and the string description stay in synch, and are easy to refactor using a tool.
  • In src/runtime/select.go there is a struct scase of which the field names must be used in the file src/cmd/compile/internal/gc, in the function scasetype(). Here nameof() could be used on the struct fields to make the compiler check at compile time that the two files are synchronized, so we don't have to rely on a comment reminder. This pattern is probably used in other places as well..
  • Someone who actually knows the Go source code could find a lot more, no doubt... ;)

I would like other people who gave thumbs up to this issue to contribute examples of how they would like to use nameof as well.

@ianlancetaylor
Copy link
Contributor

While it may be true that using nameof makes the logrus example easier to refactor, that isn't normally the most important characteristic of a piece of code. Readability is normally more important than ease of refactoring. So the example still doesn't seem entirely convincing.

Keeping the name of a function and a string description in a test seems fairly minor.

We can't really use nameof for things like runtime/select.go where the runtime and the compiler have to be in synch. For that we should use code generation, instead. We already do that partially, in cmd/compile/internal/gc/runtime, and we could do more of it if we wanted to.

Still looking for clearer examples of where this is really useful.

It's also worth noting that the ease of refactoring due to nameof can in some cases introduce a problem. If an exported name is based on a nameof, then renaming an internal name may surprisingly change the external API in an invisible way. This may not be too likely but it's worth considering.

@beoran
Copy link
Author

beoran commented Feb 25, 2020

I understand you would like more examples but l have more or less reached the point where I can't think of more ways of how I'd like to use nameof(), without having it to play around with. So as I said before, I would like others interested in this feature also provide examples.

I don't feel confident in my ability to implement a prototype for this feature myself, otherwise I would play around a bit to see how I would use nameof.

@creker
Copy link

creker commented Feb 25, 2020

I also don't have anything significant to add. Provided examples already cover pretty much everything I expect nameof to be useful for. In C# it was also solving significant pain point around implementing INotifyPropertyChanged interface. That doesn't apply to Go so we're left with everything else - logging, error augmentation, query building. All in the name of allowing safe refactoring.

@beoran
Copy link
Author

beoran commented Feb 25, 2020

On the point of refactoring, I do think that code that is easier to refactor but slightly harder to read is better than code that looks easier to read but is harder to refactor.

In general, I feel many Go programmers value ease of refactoring greatly. This is why many people program Go from an IDE, which facilitates refactoring. The nameof built in function would be a feature that helps refactoring in IDE more easily in some cases.

@ianlancetaylor
Copy link
Contributor

In the absence of compelling examples, this is a likely decline. Leaving open for four weeks for final comments.

@beoran
Copy link
Author

beoran commented Mar 11, 2020

Well, the examples we gave are perhaps enough to show that nameof() would be convenient, but I guess that to the Go developers, they do not demonstrate enough of an improvement to the Go language to warrant implementation.

I would like to ask the others who gave this issue thumbs up, or anyone else who reads this and likes the idea, again to provide some more examples as well. We will need them to convince the Go language developers that nameof() is worth while. I have proposed this issue for the benefit of the Go community, but if there is not enough interest, the Go team is well justified in pursuing more pressing issues.

@ianlancetaylor
Copy link
Contributor

No change in consensus. Closing.

@hammypants
Copy link

@beoran I know I'm mad late on this issue, and I'm not an experienced Go developer by any means (exp'd outside of Go), but if you decide to propose this in the future: testing.

Currently using testify to do some simple interface mocks, and there's a need to bind to method signatures on a type: testObj.On("DoSomething", 123).Return(true, nil) constantly. A nameof, or similar, compile-time check on the name of a signature would be extremely useful here.

I'll also just throw my 2c in for doing anything to get rid of magic-strings and sentinel value paradigms in any statically typed language-- they are faux-simpler, horrible to use at scale, and demonstrate a very weak static type system. I feel nameof is just as readable as a string for much less magic overall in this case.

For API concerns: it would probably have to function with package-level visibility, which is in line with Go in general. nameof on exported signatures when used on sigs of another package, any signature within your own package.

Also, thank you to those who proposed this for making the effort. Sorry I came so late-- just hit the frustration point enough to even Google this. :)

@beoran
Copy link
Author

beoran commented Aug 3, 2020

Well, the issue was already closed, so I'm sorry but I think it's too late now, at least for the time being. But thanks for your examples, they really show the value of a built-in nameof. If more people come in to support this idea, I might consider resubmitting this issue with improvements.

@Hellysonrp
Copy link

Hellysonrp commented Oct 30, 2020

@beoran I'm late too, but I'd like to show you my use case.
I'm using GORM to manage my database.
GORM has some filter functions that, in some cases, must receive a map[string]interface{} (column name to value mapping).
My goal is to get the column name to use in these functions.

I have a NamingStrategy (that implements schema.Namer interface) used to change column names according to the table name.
Using the NamingStrategy object, I can get the table name by passing the struct name to the TableName function. By passing the result and the struct field name to the ColumnName function, I can get the column name.
I might get the struct name with reflect, but I don't have a easy way to get a specific struct field name with it.
With nameof, I could simply do something like this:

dbColumnName := db.NamingStrategy.ColumnName(db.NamingStrategy.TableName(nameof(MyStruct)), nameof(MyStruct.MyField))

Just like that, my code is now safe from typing errors in the column names and we might change the naming strategy without needing to change many places in code.

@alaincao
Copy link

alaincao commented Jan 9, 2021

Heylow, too late as well, but frankly I could not have been sooner since I am learning Go since this week only ;-)

Being so new, I cannot give Go examples, but coming from C# I can give a little example on how it's been useful to me:
When I want to change the behaviour (not even the name) of a field (e.g. changing from "NOT NULL" to "NULL"able), I temporarily change the field name (e.g. from 'SomeField' to 'SomeFieldX') and launch the compilation.
Then I start changing the names everywhere the compiler complains there's an unknown field. That forces me to manually check the code everywhere that field is used until all compilation errors (ie. including all nameof() references) are resolved.

Like @beoran mentionned above, I too value safe refactoring much more than ease of reading.
Before C#6 (ie. before 'nameof' was available), I used to put things like const string SomeFieldName = "SomeField" right next to each fields that I referenced by name. But because of the burden that is, I was clearly the only one and in practice, more than once problems due to refactoring field names flew under the radar until crashes happened at the customers...
Now, if I ever make something serious with Go, I am personally going to put those SomeFieldName constants everywhere, and that won't ease readability either...

Also, in an IDE, being able to "Find all references" of something is so much useful.

@davecheney
Copy link
Contributor

For the latecomers, Inlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. Please direct your discussion to other forums. Thank you.

@golang golang locked as resolved and limited conversation to collaborators Jan 10, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

9 participants