Not sure if this has been brought up before. Couldn't find anything when searching through old proposals for vararg.
Roughly, I'd like to have named values for a variable argument list that maps to map[string]T in the callee.
log.Err("something went wrong", foo="bar", asdf=1)
func Err(msg string, args ...map[string]interface{}) {
// ...
}
Obviously a go2 thing given the nature of the change but I'm curious if there's interest or glaring flaws I'm missing. Happy to flush out more of the details and dig into the compiler. I'm curious what kind havoc this might have on the grammar, parser, runtime, etc.
This idea was born out of a discussion for an ergonomic API for dynamic, structured logging. As I see it there are roughly three approaches, in my personal preference order.
Best compromise of safety and readability (IMO)
type L map[string]interface{}
log.Err(err, "something went wrong", L { "foo": "bar", "asdf": 1 })
Most concise but loses a lot of safety.
log.Err(err, "something went wrong", "foo", "bar", "asdf", 1)
Safest, but find this pattern awful from a readability perspective
log.Err(err).Str("foo", "bar").Int("asdf", 1).Msg("something went wrong")
All of these left me wanting something better in terms of a cleaner API at the call-site. Hence the named varags idea.
map[string]T be supported rather than just map[string]interface{}?= make sense for binding keys to values? Or would : be better to align with map literals? Or something else entirely?TODO - Will fill this out if there's traction
May be related to #26459.
Thanks for the link, very much related to #26459 in both the rationale and original idea for implementation. Apparently I needed to search for variadic and not vararg.
I read through the thread and it seems like the main feedback is that it's mostly syntax sugar. I tend to agree with that point and can see the arguments against it. Especially if #12854 gets accepted, there's almost no added value.
However, I still think there's potential value in what I called "named varargs" (or maybe it should be "named variadic args"). Being able to seamlessly associate a name to a value at the call-site in a dynamic manner while retaining type safety is what I'm looking for. Beyond logging, I think there are other serialization contexts where this would be useful and provide a cleaner interface.
I have a rough alternative idea, quite different from my initial proposal. I think it would resolve the "syntax sugar" point as well as maintaining argument order. Need a bit of time to flush it out and write it up though.
@ianlancetaylor does it make sense to repurpose this proposal with the new idea (especially since the original is so close to #26459) or should I open a new one when it's ready?
Probably clearer to open a new one, thanks. We aren't short on issue numbers.
If name:1 or name=1 is a literal or expression of key-value pair, can it be use only in the function argument list?
In this case, go will have another design that is not uniform. If you want to look uniform, you need to assign name:1 or name=1 to variables and constants.
a := name:1
a := name=1
const a = name:1
const a = name=1
then what is the type of this key-value pair?
Look at the function signature.
func Err(msg string, args ...map[string]interface{})
a map only store a key-value pair?
func Err(msg string, args map[string]interface{})
This is not very good.
func Err(msg string, args ...[string]interface{})
func Err(msg string, args ...string:interface{})
If name:1 or name=1 is a literal or expression of key-value pair, can it be use only in the function argument list?
That was the original idea. My end goal is to "solve" verbosity at the call-site where you commonly duplicate names. Instead of
Err("something went wrong: foo=%v", foo)
func Err(msg string, args ...interface{})
I would like to do
Err("something went wrong: %v", foo)
func Err(msg string, args ...var)
And both would produce something went wrong: foo=42.
Essentially I want to "lift" the variable into something that is type-safe while also capturing the name from the call-site. I've been trying to work out a proposal for this but haven't had much time. I also keep feeling like I'm falling down some sort of hybrid generic/reflection territory.
My thought on var in the argument list is to leverage an already existing keyword. I was originally thinking of a "symbol" type drawing on ideas from Clojure/Ruby but that is purely the name piece.
Throwing this out there in case someone else wants to run with the idea (if it hasn't already been discussed). I likely won't have too much time in the coming weeks to flush it out. Maybe I'll be able to take another stab at it though.
Essentially I want to "lift" the variable into something that is type-safe while also capturing the name from the call-site.
That is traditionally the role of macros.
Have you considered using a macro preprocessor and //go:generate? That would at least allow you to do some experimenting, and having the results of that experiment would potentially make a stronger case for any future proposal along those lines.
Interesting, it's been a long time since I've done C/C++ so that didn't even cross my mind. Thanks for the idea!
I'm guessing there may be some interesting ideas in how Rust does type-safe macros that I should dig into as well.
Like #26459, this is syntactic sugar for a case that can already be handled and is not especially common.
Most helpful comment
Probably clearer to open a new one, thanks. We aren't short on issue numbers.