Skip to content

Instantly share code, notes, and snippets.

@mccutchen
Created November 18, 2020 14:29
Show Gist options
  • Select an option

  • Save mccutchen/9f1034dc12ef0fd80cac5b48dda44fed to your computer and use it in GitHub Desktop.

Select an option

Save mccutchen/9f1034dc12ef0fd80cac5b48dda44fed to your computer and use it in GitHub Desktop.

Revisions

  1. mccutchen created this gist Nov 18, 2020.
    11 changes: 11 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    # golang scoping surprise

    Here's a short snippet of code that demonstrates a surprising effect of
    golang's variable scoping rules. (This is maybe only surprising for me, with
    Python's scoping rules deeply ingrained in my brain.)

    In the real world, this came up in the context of an HTTP middleware function,
    where state accumulated in surprising ways across multiple requests to the
    handler wrapped by the middleware.

    Try it out on the Go Playground: https://play.golang.org/p/s2KHE-8Kbyd
    59 changes: 59 additions & 0 deletions scoping-surprise.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,59 @@
    package main

    import "fmt"

    func main() {
    data := Data{[]string{"main"}}
    a := wrap(data, "A", func(data Data) {
    fmt.Printf(" in func A: %#v\n", data)
    })
    b := wrap(data, "B", func(data Data) {
    fmt.Printf(" in func B: %#v\n", data)
    })

    a()
    a()
    a()
    b()
    b()

    // Output:
    // wrapping func A: main.Data{Value:[]string{"main"}}
    // wrapping func B: main.Data{Value:[]string{"main"}}
    // calling func A: main.Data{Value:[]string{"main", "A"}}
    // in func A: main.Data{Value:[]string{"main", "A"}}
    // calling func A: main.Data{Value:[]string{"main", "A", "A"}}
    // in func A: main.Data{Value:[]string{"main", "A", "A"}}
    // calling func A: main.Data{Value:[]string{"main", "A", "A", "A"}}
    // in func A: main.Data{Value:[]string{"main", "A", "A", "A"}}
    // calling func B: main.Data{Value:[]string{"main", "B"}}
    // in func B: main.Data{Value:[]string{"main", "B"}}
    // calling func B: main.Data{Value:[]string{"main", "B", "B"}}
    // in func B: main.Data{Value:[]string{"main", "B", "B"}}
    }

    func wrap(data Data, name string, f func(Data)) func() {
    fmt.Printf("wrapping func %s: %#v\n", name, data)
    return func() {
    // This modifies `data` in the outer scope, even though WithValue
    // returns a new "instance" of Data, so subsequent calls to the wrapped
    // function will accumulate additional values.
    //
    // This caught me off guard, though it probably shouldn't have.
    //
    // The fix is to use := instead of = to declare a new variable with the
    // same name that would shadow `data` in the outer scope (or just use a
    // different name altogether).
    data = data.WithValue(name)
    fmt.Printf(" calling func %s: %#v\n", name, data)
    f(data)
    }
    }

    type Data struct {
    Value []string
    }

    func (d Data) WithValue(val string) Data {
    return Data{append(d.Value, val)}
    }