Go: proposal: string interpolation

Created on 8 Sep 2019  ·  67Comments  ·  Source: golang/go

Introduction

For ones who don't know what it is:

Swift

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"

Kotlin

var age = 21

println("My Age Is: $age")

C

```c#
string name = "Mark";
var date = DateTime.Now;

Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");

# Reasoning of string interpolation vs old school formatting

I used to think it was a gimmick but it is not in fact. It is actually a way to provide type safety for string formatting. I mean compiler can expand interpolated strings into expressions and perform all kind of type checking needed.

### Examples

variable := "var"
res := "123{variable}321" // res := "123" + variable + "321"


return errors.New("opening config file: \{err}") // return errors.New("opening config file: " + err.Error())


var status fmt.Stringer

msg := "exit status: {status}" // msg := "exit status: " + status.String()


v := 123
res := "value = {v}" // res := "value = " + someIntToStringConversionFunc(v)

# Syntax proposed

* Using `$` or `{}` would be more convenient in my opinion, but we can't use them for compatibility reasons
* Using Swift `\(…)` notation would be compatible but these `\()` are a bit too stealthy

I guess  `{…}` and `\(…)` can be combined into `\{…}`

So, the interpolation of variable `variable` into some string may look like

"{variable}"

Formatting also has formatting options. It may look like

"{variable[:]}"

#### Examples of options

v := 123.45
fmt.Println("value={v:04.3}") // value=0123.450


v := "value"
fmt.Println("value='{v:a50}'") // value='<45 spaces>value'

# Conversions

There should be conversions and formatting support for built in types and for types implementing `error` and `fmt.Stringer`. Support for types implementing

type Formatter interface {
Format(format string) string
}
```

can be introduced later to deal with interpolation options

Pros and cons over traditional formatting

Pros

  • Type safety
  • Performance (depends on the compiler)
  • Custom formatting options support for user defined types

Cons

  • Complication of a compiler
  • Less formatting methods supported (no %v (?), %T, etc)
Go2 LanguageChange Proposal

Most helpful comment

For me, type-checking as a reason to include string interpolation in the language is not that compelling. There is a more important reason, which is not depending on the order in which you write the variables in fmt.Printf.

Let's take one of the examples from the proposal description and write it in Go with and without string interpolation:

  • Without string interpolation (current Go)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %v! Today is %v, it's %02v:%02v now.", name, date.Weekday(), date.Hour(), date.Minute())
  • Equivalent functionality with string interpolation (and mixing the @bradfitz comment, needed to express the formating options)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %{name}! Today is %{date.Weekday()}, it's %02{date.Hour()}:%02{date.Minute()} now.")

For me, the second version is easier to read and modify and less error-prone, as we do not depend on the order in which we write the variables.

When reading the first version, you need to re-read the line several times to check it is correct, moving your eyes back and forth to create a mental mapping between the position of a "%v" and the place where you need to put the variable.

I've been fixing bugs in several applications (not written in Go, but with the same issue) where the database queries had been written with a lot of '?' instead of named parameters (SELECT * FROM ? WHERE ? = ? AND ? != false ...) and further modifications (even inadvertently during a git merge) flipped the order of two variables 😩

So, for a language whose goal is to ease maintainability in the long term, I think this reason makes it worth it to consider having string interpolation

Regarding the complexity of this: I don't know the internals of Go compiler, so take my opinion with a grain of salt, but _Couldn't it just translate the second version showed in the above example to the first one?_

All 67 comments

Why can't we use just ${...}? Swift already has one syntax, JavaScript and Kotlin another, C# yet another, also Perl... Why invent one more variation? Wouldn't it be better to stick to the one most readable and already existing?

Why can't we use just ${...}? Swift already has one syntax, JavaScript and Kotlin another, C# yet another, also Perl... Why invent one more variation? Wouldn't it be better to stick to the one most readable and already existing?

Because we may have existing strings with ${…}. I do, for instance.

And \{ is not allowed right now.

This doesn't seem to have a big advantage over calling fmt.Sprintf.

This doesn't seem to have a big advantage over calling fmt.Sprintf.

Yes, it doesn’t besides full type safety, better performance and ease of use. Formatting done right for a language with static typing.

As far as I can tell this proposal isn't any more type safe than using fmt.Sprintf with %v. It's essentially identical with regard to type safety. I agree that a careful implementation could have better performance in many cases. I'm agnostic on ease of use.

In order to implement this we would have to write, in the language spec, the exact formatting to use for all types. We would have to decide and document how to format a slice, an array, a map. An interface. A channel. This would be a significant addition to the language spec.

I think it's one of those questions where both ways have the right to exist, and it's just up to the decision makers to decide. In Go, the decision has been already made, quite a long time ago, and it works, and it's idiomatic. That's fmt.Sprintf with %v.

Historically, there are languages where interpolation inside a string has been present from the very beginning. Notably it's Perl. That's one of the reasons why Perl became so popular, because it was a super-convenient compared to sprintf() in C et al. And the %v wasn't invented then yet. And then there are languages where the interpolation was present, kind of, but was inconvenient syntactically, think of "text" + v1 + "text". JavaScript then introduced backtick-quoted literals, which are multi-line, and support interpolation of arbitrary expressions inside ${...}, which was huge improvement compared to "text" + v1 + "text". The Go too has backtick multi-line literals, but without ${...}. Who copied whom I don't know.

I don't agree with @ianlancetaylor that support of this will need substantial effort. In fact, fmt.Sprintf with %v does exactly this, doesn't it? It looks for me like a different syntax wrapper to exactly the same thing under the hood. Am I right?

But __I do agree__ with @ianlancetaylor that using fmt.Sprintf with %v is as convenient. It's also exactly the same in length on the screen, and, what is IMHO very important - is already idiomatic for Go. It kind of makes Go Go. If we copy-implement every other feature from every other language, then it will no longer be Go, but every other language.

There's one more thing. From my long experience with Perl, where string interpolation was present from the very beginning, I can say that it's not that perfect. There's a problem with that. For simple and trivial programs, and probably for 90% of all programs, string interpolation of variables works just fine. But then, once in a while, you get a var=1.99999999999, and want to print it as 1.99, and you __can't__ do that with standard string interpolation. You either need to do some conversion beforehand, or... Look into the docs, and re-learn long forgotten sprintf() syntax. Here __is__ the problem - using string interpolation allows you to forget how to use sprintf()-like syntax, and probably it's very existence. And then when it's needed - you spend too much time and effort to do simplest things. I am talking in Perl context, and it was a decision of language designer(s) to do so.

But in Go, a different decision has been made. The fmt.Sprintf with %v is already here, it's __as convenient, as short__ as interpolated strings, and it's __idiomatic__, in that it's in the docs, in examples, everywhere. And it doesn't suffer from the problem of eventually forgetting how to print 1.99999999999 as 1.99.

Introducing proposed syntax will make Go a little more like Swift and/or more like JavaScript, and some may like that. But I think this particular syntax will not make it better, if not a little worse.

I think that existing way to print things should stay as it is. And if someone needs more - then there are templates for that.

If part of the argument here is safety at compile time, I don't agree that's a compelling argument; go vet, and by extension go test, have been flagging incorrect uses of fmt.Sprintf for a while.

It's also possible to optimize for performance today via go generate, if you really wish to do so. It's a tradeoff that's not worth it most of the time. I feel like the same applies to greatly expanding the spec; the tradeoff is generally not worth it.

Allowing function calls inside interpolated strings would be unfortunate - too easy to miss.
Not allowing them is another unnecessary special case.

If part of the argument here is safety at compile time, I don't agree that's a compelling argument; go vet, and by extension go test, have been flagging incorrect uses of fmt.Sprintf for a while. It's also possible to optimize for performance today via go generate, if you really wish to do so. It's a tradeoff that's not worth it most of the time. I feel like the same applies to greatly expanding the spec; the tradeoff is generally not worth it.

But type safeness should be guaranteed by the compiler, not by tooling. This is semantics; it is like saying that there should be a tool for verifying where you forgot to null check instead of having optionals or explicitly declared nullable values.

Apart from that - the only way for this to be safe is with dependent types. String interpolation is just more syntatic sugar for the same stuff as fmt.Sprintf and, although I'm all in favor of some good sugar, the whole go community seems not to be.

Or perhaps something like modifying the language to just work better with fmt.Printf & friends.

Like if fmt supported something like %(foo)v or %(bar)q, then say that if a string literal containing %(<ident>) is used in a call to a variadic func/method, then all the referenced symbols are appended to the variadic list automatically.

e.g. this code:

name := "foo"
age := 123
fmt.Printf("The gopher %(name)v is %(age)2.1f days old.")

would really compile to:

name := "foo"
age := 123
fmt.Printf("The gopher %(name)v is %(age)2.1f days old.", name, age)

And fmt could just skip over the unnecessary (name) and (age) bits.

That's a pretty special case language change, though.

For me, type-checking as a reason to include string interpolation in the language is not that compelling. There is a more important reason, which is not depending on the order in which you write the variables in fmt.Printf.

Let's take one of the examples from the proposal description and write it in Go with and without string interpolation:

  • Without string interpolation (current Go)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %v! Today is %v, it's %02v:%02v now.", name, date.Weekday(), date.Hour(), date.Minute())
  • Equivalent functionality with string interpolation (and mixing the @bradfitz comment, needed to express the formating options)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %{name}! Today is %{date.Weekday()}, it's %02{date.Hour()}:%02{date.Minute()} now.")

For me, the second version is easier to read and modify and less error-prone, as we do not depend on the order in which we write the variables.

When reading the first version, you need to re-read the line several times to check it is correct, moving your eyes back and forth to create a mental mapping between the position of a "%v" and the place where you need to put the variable.

I've been fixing bugs in several applications (not written in Go, but with the same issue) where the database queries had been written with a lot of '?' instead of named parameters (SELECT * FROM ? WHERE ? = ? AND ? != false ...) and further modifications (even inadvertently during a git merge) flipped the order of two variables 😩

So, for a language whose goal is to ease maintainability in the long term, I think this reason makes it worth it to consider having string interpolation

Regarding the complexity of this: I don't know the internals of Go compiler, so take my opinion with a grain of salt, but _Couldn't it just translate the second version showed in the above example to the first one?_

@ianlancetaylor pointed out that my sketch above (https://github.com/golang/go/issues/34174#issuecomment-532416737) isn't strictly backwards compatible, as there might be rare programs where this would change their behavior.

A backwards compatible variation would be to add a new type of "formatted string literal", prefixed by, say, an f:

e.g. this code:

name := "foo"
age := 123
fmt.Printf(f"The gopher %(name)v is %(age)2.1f days old.")

would really compile to:

name := "foo"
age := 123
fmt.Printf(f"The gopher %(name)v is %(age)2.1f days old.", name, age)

But then the double f (one in Printf followed by the f before the new type of string literal) would be stuttery.

I also don't understand the inner workings of the compiler, so I (perhaps foolishly) also assume that this could be implemented in the compiler, so that something like
fmt.printf("Hello %s{name}. You are %d{age}")

would compile to its equivalent current formulation.

String interpolation has the obvious benefit of higher readability (A core design decision of Go) and also scales better as the strings that one deals with become longer and more complicated (another core design decision of Go). Please notice also that using {age} gives the string context that it otherwise wouldn't have if you were only skim reading the string (and of course ignoring the type that was specified), the string could have ended "You are tall", "You are [at XXX location]", "You are working too hard" and unless you put in the mental energy to map the format method to each instance of interpolation it isn't immediately obvious what should go there. By removing this (admittedly small) mental hurdle the programmer can focus on the logic rather than the code.

The compiler implements the language spec. The language spec currently says nothing at all about the fmt package. It doesn't have to. You can write large Go programs that don't use the fmt package at all. Adding the fmt package documentation to the language spec would make it noticeably larger, which is another way of saying that it makes the language that much more complex.

That doesn't make this proposal impossible to adopt, but it is a large cost, and we need a large benefit to outweigh that cost.

Or we need a way to discuss string interpolation without involving the fmt package. This is pretty clear for values of string type, or even []byte type, but much less clear for values of other types.

I'm not in favor of this proposal partly because of what @IanLanceTaylor said above and partly because, when you try to interpolate complex expressions with formatting options, any readability advantage tends to go out of the window.

Also it's sometimes forgotten that the ability to include variadic arguments in the fmt.Print (and Println) family of functions already enables a form of interpolation. We can easily reproduce some of the examples quoted earlier with the following code which, to my mind, is just as readable:

multiplier := 3
message := fmt.Sprint(multiplier, " times 2.5 is ", float64(multiplier) * 2.5)

age := 21
fmt.Println("My age is:", age)

name := "Mark"
date := time.Now()
fmt.Print("Hello, ", name, "! Today is ", date.Weekday(), ", it's ", date.String()[11:16], " now.\n")

name = "foo"
days := 12.312
fmt.Print("The gopher ", name, " is ", fmt.Sprintf("%2.1f", days), " days old\n.")

Another reason to still add and __have__ it in the language: https://github.com/golang/go/issues/34403#issuecomment-542560071

We find @alanfo 's comments in https://github.com/golang/go/issues/34174#issuecomment-540995458 to be convincing: you can use fmt.Sprint to do a simple sort of string interpolation. The syntax is perhaps less convenient, but any approach at this would require a special marker for variables to be interpolated in any case. And, as noted, this permits arbitrary formatting of the values to be interpolated.

As noted above there is an existing way to approximately do this that even allows for formatting of the individual variables. Therefore, this is a likely decline. Leaving open for four weeks for final comments.

I'm regularly faced with building text blocks with well over 50 variables to insert. Over 70 in a few cases. This would be easy to maintain with Python's f-strings (similar to C# mentioned above). But I'm handling this in Go instead of Python for several reasons. The initial setup of fmt.Sprintf to manage these blocks is... ok. But god forbid I have to fix a mistake or modify the text in any way at all that involves moving or deleting %anything markers and their position related variables. And manually building maps to pass to template or setting up os.Expand is not a great option either. I'll take the speed (of setup) and ease of maintainability of f-strings over fmt.Sprintf any day of the week. And no, fmt.Sprint would not be hugely beneficial. Easier to setup than fmt.Sprintf in this case. But it loses much of it's meaning visually because your jumping in and out of strings. "My {age} is not {quality} in this discussion" doesn't jump in and out of strings the way that "My ", age, " is not ", quality, " in this discussion" does. Especially over the course of many tens of references. Moving text and references around is just copy and paste with f-strings. Deletions are just select and delete. Because your always within the string. This is not the case when using fmt.Sprint. It's very easy to accidentally (or necessarily) select non-string commas or double-quote string terminations and move them about breaking the formatting and requiring edits to _massage it back into place_. fmt.Sprint and fmt.Sprintf in these cases is far more time consuming and error prone than anything resembling f-strings.

That sounds like a pretty horrible task however you do it!

If I were faced with something like that, then I'd certainly be thinking in terms of text/template initially or, if it were too awkward to get my variables into a struct or map, I'd probably prefer fmt.Sprint to fmt.Sprintf but arrange the code in the following fashion:

s := fmt.Sprint(
    text1, var1,     // comment 1
    text2, var2,     // comment 2
    ....,
    text70, var70,   // comment 70
    text71,
)

Although that would take up a lot of screen real estate, it would be relatively easy to change, delete or move things about without too much risk of making a mistake.

There are some go string interpolation libraries, but without language features such as union types or generics, they are not as flexible nor fluid as in other languages:

package main

import (
    "fmt"

    "github.com/imkira/go-interpol"
)

func main() {
    m := map[string]string{
        "foo": "Hello",
        "bar": "World",
    }
    str, err := interpol.WithMap("{foo} {bar}!!!", m)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Println(str)
}

Alternatively, go.uber.org/yarpc/internal/interpolate

Once again, building a map just so I can use template/text, a 3rd party library or minimal system for os.Expand is great when you don't have 50 or more keys you need to create for variables that are already in existence for the rest of the code in question. And the only real argument against this feature is "you might forget how to properly use % formatting" and might need to spend an extra 2 minutes using Google to lookup the formatting for the one instance you absolutely need the efficiency, specificity, or whatever of fmt.Sprintf? On the other hand maybe you almost always need the efficiency, specificity, or whatever of fmt.Sprintf and never forget the formating at all because you always use it. I don't see the problem.

The other argument being it makes the language more complex? Yes, it does. Technically any addition to the language increases its complexity. And I'm all for not making the language more complex for trivial reasons. I _REALLY_ would like to see the use of in for all the ways it's used in Python. But I'd settle for just replacing := range with in. I haven't hardly touched Python in the past year and only used it for two years prior. But nine times out of ten I type in first and then fix it with := range. But I'm not going to fight over that one. := range is sufficient and obvious to reason over code you've written prior. But f-strings and their kind is something completely different. That is hard core functionality, usability, and maintainability. It absolutely should be part of the Go spec. If there was any facility to do it as a 3rd party library I'd make the library and be done with it. But to my knowledge it's not even feasible without extra expansion to the Go language. So at the very least I think that expansion to the language should be in place.

@runeimp I want to make clear that your suggestion that we could start supporting "My {age} is not {quality} in this discussion" doesn't work. That is a valid Go string today, and if we expected to interpolate age and quality that would break existing Go programs. It would have to be something more like "My \{age} is not \{quality} in this discussion". And the issues of formatting non-string values remain.

I appreciate that @ianlancetaylor. And just to be clear Python f-strings are f"My {age} is not {quality} in this discussion" or F'My {age} is not {quality} in this discussion' or any combination of any case f and a single or double quote. As mentioned, very much like how C# seems to do it. Also, while I'd prefer the Python f-string or C# style, I will absolutely accept _ANYTHING_ that facilitates a similar usage. InterPolationACTIVATE"My @$@age###^&%$ is not @$@quality###^&%$ in this discussion"INTERpolationDEACTVATEandNullify would be acceptable. Lame, but acceptable. And I am not even joking. I'd take that lame excuse for string interpolation over fmt.Sprintf any day of the week. The maintenance benefits alone would justify it.

I understand that it would help your particular case.

In https://blog.golang.org/go2-here-we-come @griesemer wrote that any change to the language should

  1. address an important issue for many people,
  2. have minimal impact on everybody else, and
  3. come with a clear and well-understood solution.

Is this an important issue for many people?

I have no idea but I'd expect it is. I think it's a feature that isn't important to anyone who's only ever had to deal with 5 reference or less on a short string. Or always had to manage it with templates anyway because of other spec requirements. But for those of us that do use it. It is quite sorely missed when not present. And for those who've never had the option (only ever used C, C++, etc. prior to Go) it may be very hard to understand the benefits on a purely theoretical level as the examples in their experience are likely quickly forgotten or only remembered as the worst projects they've had to deal with trying to forget the pain points. I doubt you'd ever see a majority response regarding the issue unless most Go developers came from languages that actually support local variable string interpolation.

It's a problem I've always run into since I started as a professional web developer over 20 years ago. In most of those cases templating was common for front-end and back-end. At the tail end of that part of my career I eventually did work in Ruby on Rails and fell instantly in love with Ruby's "My #{age} is not #{quality} in this discussion" string interpolation. Even though the pound-curly-brace syntax struck me as very odd initially. When I transitioned to integration engineering I was mostly using Python 3 and was much happier using it's new str.format() system instead of it's old % formatted strings. With that one you would do something like "My {} is not {} in this discussion".format(age, quality). So using agnostic references at least type didn't matter for 90% of use cases. Which is just simpler to comprehend and only concern yourself with the index. Also named references like "My {age} is not {quality} in this discussion".format(age=my_age_var, quality=my_quality_var). Now if the same var is references 30 times you only have to specify it once in the parameters and it's easy to keep track, copy & paste, or delete. Named parameters (as input) is another feature of Python I miss in Go. But I can live without it if needs be. But fell in love again with f-strings (introduced in Python 3.6) the moment I laid eyes on it. String interpolation has always made my life easier.

@runeimp Could you post an example of one of these 50+ variable string interpolations (in any language you have them)? I think it might help the discussion to have an actual example of the code in question.

OK, this is not final code, is a simple example, has only 50 references, and the variable names have been changed to protect the innocent.

Go fmt.Sprintf

func (dt DataType) String() string {
    MMMCodeTail := " "
    if len(pn.MMMCode) > 0 {
        MMMCodeTail = "\n\t"
    }

    MMMVCTail := " "
    if len(pn.MMMVoipCommunication) > 0 {
        MMMVCTail = "\n\t"
    }

    MMMCCTail := " "
    if len(pn.MMMCombatConditions) > 0 {
        MMMCCTail = "\n\t"
    }

    MMMSRTail := " "
    if len(pn.MMMSecurityReporting) > 0 {
        MMMSRTail = "\n\t"
    }

    MMMLKTail := " "
    if len(pn.MMMLanguagesKnown) > 0 {
        MMMLKTail = "\n\t"
    }

    return fmt.Sprintf(`ThingID=%6d, ThingType=%q, PersonID=%d, PersonDisplayName=%q, PersonRoomNumber=%q,
    DateOfBirth=%s, Gender=%q, LastViewedBy=%q, LastViewDate=%s,
    SaleCodePrior=%q, SpecialCode=%q, Factory=%q,
    Giver=%s, Manager=%q, ServiceDate=%s, SessionStart=%s, SessionEnd=%s, SessionDuration=%d,
    HumanNature=%q, VRCatalog=%v, AdditionTime=%d, MeteorMagicMuscle=%v,
    VRQuest=%q, SelfCare=%v, BypassTutorial=%q, MultipleViewsSameday=%v,
    MMMCode=%q,%sMMMVoipCommunication=%q,%sMMMCombatConditions=%q,%sMMMSecurityReporting=%q,%sMMMLanguagesKnown=%q,%sMMMDescription=%q,
    SaleCodeLatest=%q, HonoraryCode=%q, LegalCode=%q, CharacterDebuffs=%q,
    MentalDebuffs=%q, PhysicalDebuffs=%q,
    CharacterChallenges=%q,
    CharacterChallengesOther=%q,
    CharacterStresses=%q,
    RelationshipGoals=%q, RelationshipGoalsOther=%q,
    RelationshipLobsters=%q,
    RelationshipLobstersOther=%q,
    RelationshipLobsterGunslingerDoublePlus=%q,
    RelationshipLobsterGunslingerPlus=%q,
    RelationshipLobsterGunslingerGains=%q,
    PersonAcceptsRecognition=%q,
    PersonAcceptsRecognitionGunslinger=%q,
    BenefitsFromChocolate=%v, DinnerForLovelyWaterfall=%v, ModDinners=%q, ModDinnersOther=%q,
    FlexibleHaystackList=%q, FlexibleHaystackOther=%q,
    ModDiscorseSummary=%q,
    MentallySignedBy=%q, Overlord=%q, PersonID=%d,
    FactoryID=%q, DeliveryDate=%s, ManagerID=%q, ThingReopened=%v`,
        dt.ThingID,
        dt.ThingType,
        dt.PersonID,
        dt.PersonDisplayName,
        // dt.PersonFirstName,
        // dt.PersonLastName,
        dt.PersonRoomNumber,
        dt.DateOfBirth,
        dt.Gender,
        dt.LastViewedBy,
        dt.LastViewDate,
        dt.SaleCodePrior,
        dt.SpecialCode,
        dt.Factory,
        dt.Giver,
        dt.Manager,
        dt.ServiceDate,
        dt.SessionStart,
        dt.SessionEnd,
        dt.SessionDuration,
        dt.HumanNature,
        dt.VRCatalog,
        dt.AdditionTime,
        dt.MeteorMagicMuscle,
        dt.VRQuest,
        dt.SelfCare,
        dt.BypassTutorial,
        dt.MultipleViewsSameday,
        dt.MMMCode, MMMCodeTail,
        dt.MMMVoipCommunication, MMMVCTail,
        dt.MMMCombatConditions, MMMCCTail,
        dt.MMMSecurityReporting, MMMSRTail,
        dt.MMMLanguagesKnown, MMMLKTail,
        dt.MMMDescription,
        dt.SaleCodeLatest,
        dt.HonoraryCode,
        dt.LegalCode,
        dt.CharacterDebuffs,
        dt.MentalDebuffs,
        dt.PhysicalDebuffs,
        dt.CharacterChallenges,
        dt.CharacterChallengesOther,
        dt.CharacterStresses,
        dt.RelationshipGoals,
        dt.RelationshipGoalsOther,
        dt.RelationshipLobsters,
        dt.RelationshipLobstersOther,
        dt.RelationshipLobsterGunslingerDoublePlus,
        dt.RelationshipLobsterGunslingerPlus,
        dt.RelationshipLobsterGunslingerGains,
        dt.PersonAcceptsRecognition,
        dt.PersonAcceptsRecognitionGunslinger,
        dt.BenefitsFromChocolate,
        dt.DinnerForLovelyWaterfall,
        dt.ModDinners,
        dt.ModDinnersOther,
        dt.FlexibleHaystackList,
        dt.FlexibleHaystackOther,
        dt.ModDiscorseSummary,
        dt.MentallySignedBy,
        dt.Overlord,
        dt.PersonID,
        dt.FactoryID,
        dt.DeliveryDate,
        dt.ManagerID,
        dt.ThingReopened,
    )
}

A Potential F-Strings Example

func (dt DataType) String() string {
    MMMCodeTail := " "
    if len(pn.MMMCode) > 0 {
        MMMCodeTail = "\n\t"
    }

    MMMVCTail := " "
    if len(pn.MMMVoipCommunication) > 0 {
        MMMVCTail = "\n\t"
    }

    MMMCCTail := " "
    if len(pn.MMMCombatConditions) > 0 {
        MMMCCTail = "\n\t"
    }

    MMMSRTail := " "
    if len(pn.MMMSecurityReporting) > 0 {
        MMMSRTail = "\n\t"
    }

    MMMLKTail := " "
    if len(pn.MMMLanguagesKnown) > 0 {
        MMMLKTail = "\n\t"
    }

    return fmt.Print(F`ThingID={dt.ThingID}, ThingType={dt.ThingType}, PersonID={dt.PersonID}, PersonDisplayName={dt.PersonDisplayName}, PersonRoomNumber={dt.PersonRoomNumber},
    DateOfBirth={dt.DateOfBirth}, Gender={dt.Gender}, LastViewedBy={dt.LastViewedBy}, LastViewDate={dt.LastViewDate},
    SaleCodePrior={dt.SaleCodePrior}, SpecialCode={dt.SpecialCode}, Factory={dt.Factory},
    Giver={dt.Giver}, Manager={dt.Manager}, ServiceDate={dt.ServiceDate}, SessionStart={dt.SessionStart}, SessionEnd={dt.SessionEnd}, SessionDuration={dt.SessionDuration},
    HumanNature={dt.HumanNature}, VRCatalog={dt.VRCatalog}, AdditionTime={dt.AdditionTime}, MeteorMagicMuscle={dt.MeteorMagicMuscle},
    VRQuest={dt.VRQuest}, SelfCare={dt.SelfCare}, BypassTutorial={dt.BypassTutorial}, MultipleViewsSameday={dt.MultipleViewsSameday},
    MMMCode={dt.MMMCode},{MMMCodeTail}MMMVoipCommunication={dt.MMMVoipCommunication},{MMMVCTail}MMMCombatConditions={dt.MMMCombatConditions},{MMMCCTail}MMMSecurityReporting={dt.MMMSecurityReporting},{MMMSRTail}MMMLanguagesKnown={dt.MMMLanguagesKnown},{MMMLKTail}MMMDescription={dt.MMMDescription},
    SaleCodeLatest={dt.SaleCodeLatest}, HonoraryCode={dt.HonoraryCode}, LegalCode={dt.LegalCode}, CharacterDebuffs={dt.CharacterDebuffs},
    MentalDebuffs={dt.MentalDebuffs}, PhysicalDebuffs={dt.PhysicalDebuffs},
    CharacterChallenges={dt.CharacterChallenges},
    CharacterChallengesOther={dt.CharacterChallengesOther},
    CharacterStresses={dt.CharacterStresses},
    RelationshipGoals={dt.RelationshipGoals}, RelationshipGoalsOther={dt.RelationshipGoalsOther},
    RelationshipLobsters={dt.RelationshipLobsters},
    RelationshipLobstersOther={dt.RelationshipLobstersOther},
    RelationshipLobsterGunslingerDoublePlus={dt.RelationshipLobsterGunslingerDoublePlus},
    RelationshipLobsterGunslingerPlus={dt.RelationshipLobsterGunslingerPlus},
    RelationshipLobsterGunslingerGains={dt.RelationshipLobsterGunslingerGains},
    PersonAcceptsRecognition={dt.PersonAcceptsRecognition},
    PersonAcceptsRecognitionGunslinger={dt.PersonAcceptsRecognitionGunslinger},
    BenefitsFromChocolate={dt.BenefitsFromChocolate}, DinnerForLovelyWaterfall={dt.DinnerForLovelyWaterfall}, ModDinners={dt.ModDinners}, ModDinnersOther={dt.ModDinnersOther},
    FlexibleHaystackList={dt.FlexibleHaystackList}, FlexibleHaystackOther={dt.FlexibleHaystackOther},
    ModDiscorseSummary={dt.ModDiscorseSummary},
    MentallySignedBy={dt.MentallySignedBy}, Overlord={dt.Overlord}, PersonID={dt.PersonID},
    FactoryID={dt.FactoryID}, DeliveryDate={dt.DeliveryDate}, ManagerID={dt.ManagerID}, ThingReopened={dt.ThingReopened}`,
    )
}

Which of the two would you like to maintain and add, update, delete every month for the next few years?

Can I choose neither? Both look horrifying to me.

What about your case can't be done with fmt.Sprintf("%#v", dt)? That is, what's the requirement on ordering? Exact formatting (e.g. = vs : for separators, %q vs. %v, ...)? Unprinted fields? Newlines?

Is some other program parsing the output, or is this for human consumption?

What about using reflect?

v := reflect.ValueOf(dt)
t := v.Type()
for i := 0; i < t.NumField(); i++ {
    f := t.Field(i)
    s += fmt.Sprintf("%s=%v", f.Name, v.Field(i))
    if t.Field(i).Type.Kind() == reflect.String && v.Field(i).Len() > 0 {
        s += "\n\t"
    } else {
        s += " "
    }
}

Both of those options are reasonable for this very specific example. But that's not going to fly for anything more complex. Or literally anything else. Like when the same reference is used many times in the one string. Or the values are not part of a single struct or map but are generated in the same scope. This is just a cludged example. Unfortunately I can't show any real code. But really just look at any web page with hundreds of words on it. Open the source view and imagine that 50+ words need to be dynamic. Now imagine this is not for a web page, you don't need a template system for any other reason but to possibly solve this problem, all the variables are already in scope being used in other parts of the code and/or in multiple places in this code block, and this text may be changing every 3 to 4 weeks in sometimes large and very significant ways. This may be part of a system that _occassionally_ needs to generate any or all of PDF, CSV, SQL INSERT, email, an API call, etc.

Without real-world examples, it's really hard to tell whether this proposal is the right solution to the problem. Maybe string interpolation really is the solution, or maybe we're running into the XY problem. Maybe the right solution is already in the language somewhere, or the solution is a change to package reflect, or perhaps package text/template.

Firstly, I doubt whether this is an important issue for many people. This is the only serious discussion I can recall seeing about it and the emoji voting doesn't suggest overwhelming support. Third party libraries are not exactly plentiful either.

Secondly, it's worth pointing out that even fmt.Printf (and family) can do repeated arguments:

fmt.Printf("R%s T%[1]s T%[1]s was a canine movie star\n", "in")

Thirdly, I don't think it's necessarily the case that people who've used string interpolation in other languages would want to see it in Go. I've used it before in several languages and, whilst it's fine when you want to interpolate simple variables, as soon as you try to use more complex expressions, function calls, escaped flag characters etc. and then add formatting details to all of that (which inevitably means a 'printf' style approach) the whole thing can quickly become an unreadable mess.

Finally, it seems to me that, when you have a large number of variables to interpolate, then text/template is the best approach and perhaps therefore we should be looking at ways of making that more convenient to use when the variables are not part of a well-defined structure rather than integrating something of this complexity into the language itself.

Alternatively, build a DSL:

func (dt *DataType) String() string {
    var s strings.Builder
    _ = fields.Format(&s, 
        fields.PaddedInt("ThingID", 6, dt.ThingID),
        fields.String("ThingType", dt.ThingType),
        fields.String("PersonID", dt.PersonID),
        fields.Time("DateOfBirth", dt.DateOfBirth),
        ...
    )
    return s.String()
}

or

func (dt *DataType) String() string {
    var s fields.Builder
    s.PaddedInt("ThingID", 6, dt.ThingID),
    s.String("ThingType", dt.ThingType),
    s.String("PersonID", dt.PersonID),
    s.Time("DateOfBirth", dt.DateOfBirth),
    return s.String()
}

I can deeply understand @ianlancetaylor 's opinion. We know that we can live with fmt. Printf as we always have.

At the same time, I couldn't agree more with @alanfo 's opinion.

easier to read and modify and less error-prone

It is very important for robust system programming.

I think adopting features/ideas from other languages isn't a shame thing at all when it can save a lot of precious time of people. Actually, a lot of people are suffering from string interpolation problems.
There is the reason why other modern languages are adopting string interpolation.

I don't want Go remains just a little bit better language than C.
I think Go v2 is a chance to be much better Go.

image

IMHO, if the string interpolation feature is provided, most of us will choose to use it. We can feel it.

ps.
Unfortunately, @runeimp 's case is too edge case. I can feel the pain of the circumstance. But it blurs the proposal ideas. (No offense)

@doortts no offense taken. I always appreciate honest conversation.

I knew that posting real code could make a dent in the argument. And I also knew that posting an approximation that needs a little creativity to expand on mentally was probably going to just muddy the discussion. So I took a gamble because I don't expect anyone else to do it. But sharing real code is not an option and the hours it would take to put this in black and white with a concrete example, but otherwise to which I have zero use for, is just not something I have the energy for right now. I already well know this argument is uphill because of the reasons I already stated prior. I know I'm not one of a dozen people that see the real value in this feature. But most of those are almost certainly nowhere near this thread. I'm only here because I happened to see a post about it on Reddit. Otherwise I was almost completely unaware of this process. I've been pretty happy with how things have been moving forward with Go without feeling the need to jump in on the conversation. But I wanted to help with this discussion because I knew there just wouldn't be many voices fighting for it simply due to the amount of inertia to overcome whenever any change to the language is suggested. I don't think I've ever seen such strong opposition to changes in a language ever. And that makes it mighty intimidating to post in support of something new. I'm not saying any language should accept all new feature requests without review. Just that the reason it may seem quiet on the support side isn't always because the desire isn't there. And with that I ask that anyone interested in this feature please post _something_. Anything, so we can see some real numbers on this.

I think because it's the holidays, there is less attention in general. String interpolation is extremely useful and desired from a developer standpoint (which is why it is in so many other languages), but it seems this proposal is under cooked for such a large change at this point, which if rejected I don't think it means it isn't worth revisiting in the future. The small incremental changes that would make this obvious are not in place yet.

Dynamically resolving variables by name from the scope is not something that I think is possible (although yaegi Eval seems to do it?) presently.

I think the post @runeimp is referencing is my post in r/golang https://www.reddit.com/r/golang/comments/d1199a/why_is_there_no_equivalent_to_f_strings_in_python/
Where there is a bit more discussion.

I just wanted to throw my hat into the ring and say that variable interpolation would be massively helpful to me, coming from the python world something like fmt.sprintf makes Go seem seriously clunky and horrible to read/maintain.

Go is mostly very elegant in how it makes doing the things that it values very easy and powerful. Readability and maintainability are some of the core tenants of its philosophy and it really shouldn't be this difficult to read a string and know what's going into it. There is a seriously non trivial amount of overhead that comes with understanding what will be printed whilst keeping in mind what variables are mapped to what items in a list at the end of that string. We simply shouldn't need to maintain that mental overhead.

If people feel that fmt.sprintf is appropriate for them it can be used. I don't feel that having both detracts from Go as a language since the proposal is unquestionably more legible than the current method, intuitive, and easier to maintain. This shouldn't need to be justified by interpolations of 50+ variables, it's an improvement with interpolations of even just one variable.

Is something like

fmt.sprintf("I am %<type name here>{age} years old.")

# or
fmt.sprintf("I am %T{age} years old")

really a damaging prospect to the language? I'm happy to be proved wrong, but I honestly only see upside in this (or something like this) proposal, except for backwards incompatibility, which is where something like implementing this in a new major version of Go makes sense

Could it be considered to leave this discussion open for one more set of 4 weeks, since a sizable portion of the community was on vacation for Thanksgiving, Christmas and New Years when the cutoff was proposed?

There has been a bunch more discussion since https://github.com/golang/go/issues/34174#issuecomment-558844640, so I'm taking this back out of final-comment-period.

I am inclined to agree with @randall77 that I have not yet seen a compelling example here. @runeimp, thanks for posting the example code, but it seems difficult to read and difficult to change either way. As @egonelbre suggests, if we want to make this more maintainable, the first step seems to be to find an entirely different approach.

@cyclingwithelephants fmt.sprintf("I am %T{age} years old") is not on the table here. The language does not provide any mechanism that fmt.Sprintf could use to resolve age. Go is a compiled language, and local variable names are not available at execution time. This would be more palatable if we could figure out some way to make that work, perhaps along the lines of @bradfitz's suggestion above.

Thank you @ianlancetaylor for lifting the final-comment-period label. 😀

I think @bradfitz's idea was great. I'd guess there are potential limitations no matter what without a locals variable context but I'd happily accept those limitations over not having string interpolation. While I have mad respect for the updates in percentage formatting in Go (I love the additions of %q, %v, and %#v) that paradigm is ancient. Just because it's venerable does not mean is the best way to do things. Just like Go's way of handling compiling, and especially cross-compiling is _WAY_ better than how it's done with C or C++. Now, does Go just hide all the nasty compiler options with sane defaults and it's just as ugly under-the-hood. I don't know specifically but I believe that is the case. And that is fine. That is completely acceptable to me. I don't care what dark ritual the compiler does to make the feature work. I just know the feature makes life easier. And has made my life easier as a developer in every language that I've used that supports it. And is always a pain in languages that don't support it. It's significantly easier to remember than how to use the 30+ special characters in percent formatting for 98% of the string formatting I need. And saying to use %v instead of "the correct percentage format" is not the same ease of use for creation nor even close the same ease of maintenance.

Now that there is a bit more time I'll work on an example and see if I can find some articles that are little more enlightening than I have been to help illustrate the significant benefits to work efficiency for those of us who deal in human interfacing, document and code generation, and string manipulation on a regular basis.

Here is a thought that may lead to something implementable. Though I don't know that it is a good idea.

Add a new string type m"str" (and perhaps the same with a raw string). This new kind of string literal evaluates to a map[string]interface{}. Looking up the empty string in the map gives you the string literal itself. The string literal may contain expressions in braces. An expression in braces is evaluated as though it were not in the string literal, and the value is stored in the map with the key being the substring that appears within the braces.

For example:

    i := 1
    m := m"twice i is {i * 2}"
    fmt.Println(m[""])
    fmt.Println(m["i * 2"])

This will print

twice i is {i * 2}
2

Within the string literal, braces may be escaped with a backslash to indicate a simple brace. An unquoted, unmatched, brace is a compilation error. It is also a compilation error if the expression within the braces cannot be compiled. The expression must evaluate to exactly one value but is otherwise unrestricted. The same braced string may appear multiple time in the string literal; it will be evaluated as many times as it appears, but only one of the evaluations will be stored in the map (because they will all have the same key). Exactly which one is stored is unspecified (this matters if the expression is a function call).

By itself this mechanism is peculiar yet useless. Its advantage is that it can be clearly specified and arguably does not require excessive additions to the language.

The use comes with additional functions. The new function fmt.Printfm will work exactly like fmt.Printf, but the first argument will not be a string but rather a map[string]interface{}. The "" value in the map will be a format string. The format string will support, besides the usual % things, a new {str} modifier. The use of this modifier will mean that instead of using an argument for the value, str will be looked up in the map, and that value will be used.

For example:

    hi := "hi"
    fmt.Printfm(m"%20{hi}s")

will print the string hi passed out to 20 spaces.

Naturally there will be the simpler fmt.Printm which will substitute for each braced expression the contained value as printed by fmt.Print.

For example:

    i, j := 1, 2
    fmt.Printm(m"i: {i}; j: {j}")

will print

i: 1; j: 2

Problems with this approach: the odd use of an m prefix before a string literal; the duplicated m in normal use--one before and one after the parenthesis; the general uselessness of an m-string when not used with a function that expects one.

Advantages: not too hard to specify; supports both simple and formatted interpolation; not limited to fmt functions, so may work with templates or unanticipated uses.

If it were a typed map (e.g. runtime.StringMap) then we could use fmt.Print and Println without adding the stuttery Printfm

Using a defined type is a good idea, but it wouldn't help with fmt.Printfm; we couldn't use the defined type as the first argument to fmt.Printf, since that only takes a string.

One thing which was mentioned earlier in the thread by @runeimp but hasn't been fully discussed is os.Expand:-

package main

import (
    "fmt"
    "os"
)

func main() {
    name := "foo"
    days := 12.312
    type m = map[string]string
    f := func(ph string) string {
        return m{"name": name, "days": fmt.Sprintf("%2.1f", days)}[ph]
    }
    fmt.Println(os.Expand("The gopher ${name} is ${days} days old.", f))
    // The gopher foo is 12.3 days old.
}

Although this is too verbose for simple cases, it's much more palatable when you have a largish number of values to be interpolated (though any approach has a problem with 70 values!). Advantages include :-

  1. If you use a closure for the mapping function, it deals fine with local variables.

  2. It also deals fine with arbitrary formatting and keeps that out of the interpolated string itself.

  3. If you use a placeholder in the interpolated string which isn't present in the mapping function it automatically gets replaced with an empty string.

  4. Changes to the mapping function are relatively easy to make.

  5. We already have it - no languge or library changes are necessary.

@ianlancetaylor that solution sounds like a solid option to me. Though I'm not seeing why we need alternate Print methods. I'm likely overlooking something but seems a simple signature change using interface{} and type check. OK, just realized how the signature change could prove very problematic for some existing code. But if the basic mechanism was implemented and and we also created a stringlit type that represents string or m"str" or if m"str" also accepted string would that be an acceptable breaking change for Go v2? They are both "string literals", it's just that one of them essentially has a flag that allows for extra functionality, no?

Thanks for bringing that up again @alanfo, those are all excellent points. 😃

I've used os.Expand for light templating and it can be very handy in situations where you need to build a map of values anyway. But if the map isn't needed, and you would need to make your closure in several different areas just to capture the local variables for your (now copied many times) replacement function ignores DRY entirely and can will lead to maintenance issues and just adds more work were interpolated strings would "just work", alleviate those maintenance issues, and not require the building of a map just to manage that dynamic string.

@runeimp We can't change the signature of fmt.Printf. That would break Go 1 compatibility.

The notion of a stringlit type implies changing Go's type system, which is a much bigger deal. Go intentionally has a very simple type system. I don't think we want to complicate it for this feature. And even if we did, fmt.Printf would still take a string argument, and we can't change that without breaking existing programs.

@ianlancetaylor Thanks for the clarification. I appreciate the desire to not break backwards compatibility with something as fundamental as the fmt package or the type system. I was just hoping there might be some hidden (to me) possibility that might be an option along those lines somehow. 👼

I really like the Ian way to implement this. Wouldn't generics help with the fmt.Print issue?

contract printable(T) {
  T string, map[string]string // or the type Brad suggested "runtime.StringMap"
}

// And then change the signature of fmt.Print to:
func Print(type T printable) (str T) error { 
  // ...
}

This way, the Go 1 compatibility should be preserved.

For Go 1 compatibility, we can't change the type of a function at all. Functions are not only called. They are also used in code like

    var print func(...interface{}) = fmt.Print

People write code like this when making tables of functions, or when using hand-rolled dependency injection for tests.

I have the feeling that strings.Replacer (https://golang.org/pkg/strings/#Replacer) can almost do string interpolation, just missing the interpolation identifier (e.g. ${...}) and the pattern processing (e.g. if var i int = 2, "${i+1}" should be mapped to "3" in the replacer)

Yet another approach would have a built-in function, say, format("I am a %(foo)s %(bar)d") that expands to fmt.Sprintf("I am a %s %d", foo, bar). At least, that's fully backwards compatible, FWIW.

From a language design perspective, it would be peculiar to have a builtin function expand to a reference to a function in the standard library. To provide a clear definition for all implementations of the language, the language spec would have to fully define the behavior of fmt.Sprintf. Which I think we want to avoid.

This probably won't make everyone happy but I think the below would be the most general. It's broken up into three parts

  1. fmt.Printm functions that take a format string and a map[string]interface{}
  2. accept #12854 so you can drop the map[string]interface{} when calling it
  3. allow unkeyed names in map literals as shorthand for "name": name, or "qual.name": qual.name,

Taken together that would allow something like

fmt.Printm("i: {i}; j: {j}", {i, j})
// which is equivalent to
fmt.Printm("i: {i}; j: {j}", map[string]interface{}{
  "i": i,
  "j": j,
})

That still has the duplication between the format string and the arguments but it's a lot lighter on the page and it's a pattern that's easily automated: an editor or tool could automatically fill in the {i, j} based on the string and the compiler would let you know if they're not in scope.

That doesn't let you do computations within the format string which can be nice, but I've seen that overdone enough times to consider it a bonus.

Since it applies to map literals in general it can be used in other cases. I often name my variables after the key they'll be in the map I'm building.

A downside of this is that it can't apply to structs since those can be unkeyed. That could be rectified by requiring a : before the name like {:i, :j} and then you could do

Field2 := f()
return aStruct{
  Field1: 2,
  :Field2,
}

Do we need any language support for this? As go is now, it can look like this, either with a map type or with a fluid, more type-safe API:

package main

import (
    "fmt"
    "strings"
)

type V map[string]interface{}

func Printm(format string, args V) {
    for k, v := range args {
        format = strings.ReplaceAll(format, fmt.Sprintf("{%s}", k), fmt.Sprintf("%v", v))
    }
    fmt.Print(format)
}

type Buf struct {
    sb strings.Builder
}

func Fmt(msg string) *Buf {
    res := Buf{}
    res.sb.WriteString(msg)
    return &res
}

func (b *Buf) I(val int) *Buf {
    b.sb.WriteString(fmt.Sprintf("%v", val))
    return b
}

func (b *Buf) F(val float64) *Buf {
    b.sb.WriteString(fmt.Sprintf("%v", val))
    return b
}

func (b *Buf) S(val string) *Buf {
    b.sb.WriteString(fmt.Sprintf("%v", val))
    return b
}

func (b *Buf) Print() {
    fmt.Print(b.sb.String())
}

func main() {
    Printm("Hello {k} {i}\n", V{"k": 22.5, "i": "world"})
    Fmt("Hello ").F(22.5).S(" world").Print()
}

https://play.golang.org/p/v9mg5_Wf-qD

Ok, it's still inefficient, but it looks like it is not a lot of work at all to make a package that supports this. As a bonus, I included a different fluid API that might be said to simulate interpolations somewhat as well.

The "map string" proposal from @ianlancetaylor (although I personally prefer "value/variable string" with a v"..." syntax) also allows non-formatting use cases. For example, #27605 (operator overloading functions) largely exists because it is difficult today to make a readable API for math/big and other numeric libraries. This proposal would allow the function

func MakeInt(expression map[string]interface{}) Int {...}

Used as

a := 5
b := big.MakeInt(m"100000")
c := big.MakeInt(m"{a} * ({b}^2)")

Importantly, this helper function can coexist with the more performant and powerful API that currently exists.

This approach allows the library to perform whatever optimizations it wants for large expressions, and might also be a useful pattern for other DSLs, since it allows custom expression parsing while still representing values as Go variables. Notably, these use cases are not supported by Python's f-strings because the interpretation of the enclosed values is imposed by the language itself.

@HALtheWise Thanks, that is pretty neat.

I wanted to comment to show a little support for this proposal, from the stance of a general developer. I've been coding with golang for over 3 years professionally. When I moved to golang (from obj-c/swift) I was disappointed that string interpolation was not included. I've used C and C++ for over a decade in the past, so printf wan't a particular adjustment, other than feeling like going backwards a little -- I've found that it does indeed make a difference with code maintenance and readability for more complex strings. I've recently done a little bit of kotlin (for a gradle build system), and using string interpolation was a breath of fresh air.

I think string interpolation can make string composition more approachable for those new to the language. It's also a win for technical UX and maintenance, due to the reduction of cognitive load both when reading and writing code.

I am glad that this proposal is getting real consideration. I look forward to the resolution of the proposal. =)

If I understand correctly, the @ianlancetaylor proposal is:

i := 3
foo := m"twice i is %20{i * 2}s :)"
// the compiler will expand to:
foo := map[string]interface{}{
    "": "twice i is %20{i * 2}s :)",
    "i * 2": 6,
}

After that, a print function will handle that map, parse entire template again, and take few advantages of the pre-parsed template

But if we expand m"str" to a function?

i := 3
foo := m"twice i is %20{i * 2}s :)"
// the compiler will expands to:
foo := m(
    []string{"twice i is ", " :)"}, // split string
    []string{"%20s"},               // formatter for each value
    []interface{}{6},               // values
)

This function has the following signature:

func m(strings []string, formatters []string, values []interface{}) string {}

This function will perform better because, to take more advantage of the pre-parsed template, and much more optimizations could be done similar as Rust does with the println! function.

What I'm trying to describe here is very similar to the Tagged functions of the Javascript, and we could discuss whether the compiler should accept user functions to format string ex:

foo.GQL"query { users{ %{expectedFields} } }"

bla.SQL`SELECT *
    FROM ...
    WHERE FOO=%{valueToSanitize}`

@rodcorsi If I'm reading your suggestion correctly, it requires building fmt.Printf formatting into the language proper, because the compiler will have to understand where %20s starts and ends. That is one of the things I was trying to avoid.

Also note that my suggestion is not at all tied to fmt.Printf formatting, and can be used for other kinds of interpolation as well.

I would be opposed to treating m"..." as expanding to a function call, because it obscures what's actually going on, and adds what's effectively a second syntax for function calls. It does generally seem reasonable to pass a more structured representation than a map, to avoid needing matching reimplementations of the parsing behavior everywhere. Perhaps a simple struct with a slice of constant string sections, a slice of strings for things in braces, and an interface slice?

m"Hello {name}" -> 
struct{...}{
    []string{"Hello ", ""},
    []string{"name"},
    []interface{}{"Gopher"}

The second and third slices must be the same length, and the first must be one longer. There are other ways to represent this as well to encode that constraint structurally.
The advantage this has over a format that directly exposes the original string is that there's a looser requirement to have a correct and performant parser in the function that consumes it. If there's no support for escaped characters or nested m-strings, it probably isn't a big deal, but I'd rather not need to reimplement and test that parser, and caching it's result could cause runtime memory leaks.

If "formatting options" is a frequent desire of things using this syntax, I could see there being a place for them in the spec, but I'd personally go with a syntax like m"{name} is {age:%.2f} years old" where the compiler just passes everything after the : on to the function.

Hello I wanted to comment on this to add support to this proposal. I been working with many different languages in the past 5 years (Kotlin, Scala, Java, Javascript, Python, Bash, some C, etc) and I'm learning Go now.

I think string interpolation is a must have in any modern programming language, the same way as type inference is, and we have that in Go.

For those arguing that you can accomplish the same thing with Sprintf, then, I don't understand why we have type inference in Go, you could accomplish the same thing writing the type right? Well, yes, but the point here is that string interpolation reduce a lot of the verbosity you need to accomplish that and is more easy to read (with Sprintf you have to jump to the argument list and the string back and forth to make sense of the string).

In real life software, this is a much appreciate feature.

It is against the Go minimalist design? No, it's not a feature that allow you to do crazy things or abstractions that complicate your code (like inheritance), is just a way of writing less and add clarity when you read the code, which I believe isn't against what Go is trying to do (we have type inference, we have the := operator, etc).

Formatting done right for a language with static typing

Haskell has libraries and language extensions for string interpolation. It's not a type thing.

Was this page helpful?
0 / 5 - 0 ratings