golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.08k stars 17.55k forks source link

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

Closed beoran closed 4 years ago

beoran commented 4 years ago

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

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.

creker commented 4 years ago

// 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 commented 4 years ago

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

ianlancetaylor commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

Can't this be already done via reflect?

beoran commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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

creker commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

beoran commented 4 years ago

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 commented 4 years ago

No change in consensus. Closing.

hammypants commented 4 years ago

@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 commented 4 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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.