The use of context in Go can help you pass metadata through your program with helpful, related information about a call. Let’s build an example where we set a context key called "stack" which keeps a history of the function names called over the lifetime of the context. As we pass the context object through a few layers of functions, we’ll append the name of the function to the value of the context key "stack".

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background()
    Handler(ctx)

}

func Handler(ctx context.Context) {
    ctx = buildStackContext(ctx, "Handler")
    Service(ctx)
}

func Service(ctx context.Context) {
    ctx = buildStackContext(ctx, "Service")
    Gateway(ctx)
}

func Gateway(ctx context.Context) {
    ctx = buildStackContext(ctx, "Gateway")
    // print the final value of "stack" on the context
    fmt.Println(ctx.Value("stack"))
}

func buildStackContext(ctx context.Context, name string) context.Context {
    // check if "stack" is initialized on the context
    value, ok := ctx.Value("stack").(string)
    if !ok {
        return context.WithValue(ctx, "stack", name)
    }
    return context.WithValue(
        ctx,
        "stack",
        fmt.Sprintf("%s:%s", value, name),
    )
}

https://play.golang.org/p/Q-2AmWQ-bf6

In the code above, we initialize an empty context in our main function, then pass it down into some methods: Handler, Service and Gateway respectively. In Gateway, we print the final value of "stack" on the context object, which is Handler:Service:Gateway. You’ll notice we’ve had to hardcode the names of the functions ourselves which are appended to the "stack" context variable when we explicitly pass them into buildStackContext. However, we can do better. By inspecting the Go runtime, we can programatically look up the name of the function that calls buildStackContext and append that to the "stack" variable in the context:

package main

import (
    "context"
    "fmt"
    "runtime"
    "strings"
)

func main() {
    ctx := context.Background()
    Handler(ctx)

}

func Handler(ctx context.Context) {
    ctx = buildStackContext(ctx)
    Service(ctx)
}

func Service(ctx context.Context) {
    ctx = buildStackContext(ctx)
    Gateway(ctx)
}

func Gateway(ctx context.Context) {
    ctx = buildStackContext(ctx)
    // print the final value of "stack" on the context
    fmt.Println(ctx.Value("stack"))
}

func buildStackContext(ctx context.Context) context.Context {
    var name string
    // inspect the runtime for the name of the caller
    pc, _, _, ok := runtime.Caller(1)
    details := runtime.FuncForPC(pc)
    if ok && details != nil {
        name = details.Name()
        // break down the name,
        // otherwise the package name will be included as well
        // for example: "main.Handler"
        split := strings.Split(name, ".")
        name = split[len(split)-1]
    }
    
    // check if "stack" is initialized on the context
    value, ok := ctx.Value("stack").(string)
    if !ok {
        return context.WithValue(ctx, "stack", name)
    }

    return context.WithValue(
        ctx,
        "stack",
        fmt.Sprintf("%s:%s", value, name),
    )
}

https://play.golang.org/p/AXOPYBr5SKF

The above yields the same output, Handler:Service:Gateway, but allows us to arbitrarily add more function calls or change function names and still get the expected stack of function calls:

package main

import (
    "context"
    "fmt"
    "runtime"
    "strings"
)

func main() {
    ctx := context.Background()
    Consumer(ctx)

}

func Consumer(ctx context.Context) {
    ctx = buildStackContext(ctx)
    Service(ctx)
}

func Service(ctx context.Context) {
    ctx = buildStackContext(ctx)
    Gateway(ctx)
}

func Gateway(ctx context.Context) {
    ctx = buildStackContext(ctx)
    Client(ctx)
}

func Client(ctx context.Context) {
    ctx = buildStackContext(ctx)
    // print the final value of "stack" on the context
    fmt.Println(ctx.Value("stack"))
}

func buildStackContext(ctx context.Context) context.Context {
    var name string
    // inspect the runtime for the name of the caller
    pc, _, _, ok := runtime.Caller(1)
    details := runtime.FuncForPC(pc)
    if ok && details != nil {
        name = details.Name()
        // break down the name, otherwise the package name will be included as well
        // for example: "main.Handler"
        split := strings.Split(name, ".")
        name = split[len(split)-1]
    }

    // check if "stack" is initialized on the context
    value, ok := ctx.Value("stack").(string)
    if !ok {
        return context.WithValue(ctx, "stack", name)
    }

    return context.WithValue(
        ctx,
        "stack",
        fmt.Sprintf("%s:%s", value, name),
    )
}

https://play.golang.org/p/Eb8eZ5AfWke

The above prints: Consumer:Service:Gateway:Client. Using context in this way can provide useful namespacing in logs, making it easier to distinguish homogenous log statements.