An open API service indexing awesome lists of open source software.

https://github.com/eiiches/go-gen-proxy

Proxy code generation for Go interfaces
https://github.com/eiiches/go-gen-proxy

go go-generate golang interceptor

Last synced: 9 months ago
JSON representation

Proxy code generation for Go interfaces

Awesome Lists containing this project

README

          

go-gen-proxy
============

Proxy code generation for Go interfaces

Features
--------

This tool generates a Go struct that,
* implements specified Go interfaces
* calls `Invoke(method string, args []interface{})` on a specified handler upon method invocations

Even though the feature is tiny, this fills what is missing in Go reflection to effortlessly implement an interceptor pattern, etc. and can be used in many ways (see the [Examples / Use Cases](#examples--use-cases) below). If you are familiar with Java, this is similar to [Dynamic Proxy](https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html) but with code generation.

Tutorial
--------

1. Create a new project (if you don't have a project already).

```console
$ mkdir test
$ cd test
$ go mod init github.com/foo/bar
```

2. Define an interface you want to generate a proxy for. Save this as `pkg/greeter/interface.go`.

```go
package greeter

type Greeter interface {
SayHello(name string) (string, error)
}
```

You can also use a tool like [ifacemaker](https://github.com/vburenin/ifacemaker) to extract and generate an interface from existing structs.

3. Generate a proxy struct that implements the interface.

```console
$ go get github.com/eiiches/go-gen-proxy/cmd/go-gen-proxy
$ go run github.com/eiiches/go-gen-proxy/cmd/go-gen-proxy \
--interface github.com/foo/bar/pkg/greeter.Greeter \
--package github.com/foo/bar/pkg/greeter \
--name GreeterProxy \
--output pkg/greeter/greeter_proxy.go
```


View generated pkg/greeter/greeter_proxy.go

```go
// Code generated by go-gen-proxy; DO NOT EDIT.
package greeter

import (
"fmt"
"github.com/eiiches/go-gen-proxy/pkg/handler"
)

type GreeterProxy struct {
Handler handler.InvocationHandler
}

func (this *GreeterProxy) SayHello(arg0 string) (string, error) {
args := []interface{}{
arg0,
}

rets := this.Handler.Invoke("SayHello", args)

ret0, ok := rets[0].(string)
if rets[0] != nil && !ok {
panic(fmt.Sprintf("%+v is not a valid string value", rets[0]))
}
ret1, ok := rets[1].(error)
if rets[1] != nil && !ok {
panic(fmt.Sprintf("%+v is not a valid error value", rets[1]))
}

return ret0, ret1
}
```

Preferably, you can put a comment below somewhere (don't forget to adjust --output path) on the source and run `go generate`, instead of running the tool manually.

```go
//go:generate go run github.com/eiiches/go-gen-proxy/cmd/go-gen-proxy --interface github.com/foo/bar/pkg/greeter.Greeter --package github.com/foo/bar/pkg/greeter --name GreeterProxy --output pkg/greeter/greeter_proxy.go
```

Also, we recommend creating `tools/tools.go` in order to fix the version of go-gen-proxy we use. See [How can I track tool dependencies for a module?](https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module)

```go
// +build tools
package tools

import _ "github.com/eiiches/go-gen-proxy/cmd/go-gen-proxy"
```

4. Let's use the generated proxy. Save this as `cmd/tutorial/main.go`.

```go
package main

import (
"fmt"

"github.com/foo/bar/pkg/greeter"
)

type handler struct{}

func (*handler) Invoke(method string, args []interface{}) []interface{} {
switch method {
case "SayHello":
return []interface{}{fmt.Sprintf("Hello %s!", args[0]), nil}
default:
panic("not implemented")
}
}

func main() {
p := &greeter.GreeterProxy{
Handler: &handler{},
}
msg, err := p.SayHello("James")
fmt.Println(msg, err)
}
```

```console
$ go run ./cmd/tutorial
Hello James!
```

In this tutorial, we implemented a simple SayHello method via the handler, to see how the interface method invocation (`p.SayHello("James")`) is translated to a `Invoke(method string, args[]interface{})` call on the handler object and how the return values (`[]interface{}{"Hello James!", nil}`) are translated back to `(string, error)`.

This example itself is a bit useless but by customizing the handler, especially by combining it with the power of [reflection](https://pkg.go.dev/reflect), you can do much more useful things, such as intercepting method calls for logging/metrics, injecting errors, or recording every method calls for testing.

Examples / Use Cases
--------------------

Log every method calls on the interface


```go
package main

import (
"fmt"

"github.com/eiiches/go-gen-proxy/examples"
"github.com/eiiches/go-gen-proxy/pkg/interceptor"
"github.com/foo/bar/pkg/greeter"
)

type GreeterImpl struct{}

func (this *GreeterImpl) SayHello(name string) (string, error) {
return fmt.Sprintf("Hello %s!", name), nil
}

func main() {
p := &greeter.GreeterProxy{
Handler: &interceptor.InterceptingInvocationHandler{
Delegate: &GreeterImpl{},
Interceptor: &examples.LoggingInterceptor{},
},
}
msg, err := p.SayHello("James")
fmt.Println(msg, err)
}
```

```console
$ go run .
ENTER: receiver = &{}, method = SayHello, args = [James]
EXIT: receiver = &{}, method = SayHello, args = [James], retvals = [Hello James! ]
Hello James!
```

Fail a method call by some probability


```go
package main

import (
"fmt"
"math/rand"
"time"

"github.com/eiiches/go-gen-proxy/examples"
"github.com/eiiches/go-gen-proxy/pkg/interceptor"
"github.com/foo/bar/pkg/greeter"
)

type GreeterImpl struct{}

func (this *GreeterImpl) SayHello(name string) (string, error) {
return fmt.Sprintf("Hello %s!", name), nil
}

func main() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
p := &greeter.GreeterProxy{
Handler: &interceptor.InterceptingInvocationHandler{
Delegate: &GreeterImpl{},
Interceptor: &examples.ErrorInjectingInterceptor{
Random: r,
FailureProbability: 0.5,
},
},
}
msg, err := p.SayHello("James")
fmt.Println(msg, err)
}
```

```console
$ go run .
Hello James!
$ go run .
injected error
```

Mock with Testify


```go
package main

import (
"fmt"

"github.com/foo/bar/pkg/greeter"
"github.com/stretchr/testify/mock"
)

type MockHandler struct {
mock.Mock
}

func (this *MockHandler) Invoke(method string, args []interface{}) []interface{} {
return this.Mock.MethodCalled(method, args...)
}

func main() {
mock := &MockHandler{}
p := &greeter.GreeterProxy{
Handler: mock,
}
mock.On("SayHello", "James").Return("Hello James!", nil)

msg, err := p.SayHello("James")
fmt.Println(msg, err)
}
```

```console
$ go run .
Hello James!
```

Instrument method calls and expose Prometheus metrics


```go
package main

import (
"fmt"
"log"
"math/rand"
"net/http"
"reflect"
"time"

"github.com/eiiches/go-gen-proxy/pkg/interceptor"
"github.com/foo/bar/pkg/greeter"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

// Interceptor

type PrometheusInterceptor struct {
CallDurations *prometheus.HistogramVec

CallsTotal *prometheus.CounterVec
CallErrorsTotal *prometheus.CounterVec
}

func NewPrometheusInterceptor(namespace string) *PrometheusInterceptor {
return &PrometheusInterceptor{
CallDurations: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Name: "call_duration_seconds",
Help: "time took to complete the method call",
Buckets: prometheus.DefBuckets,
},
[]string{"method"},
),
CallsTotal: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "calls_total",
Help: "the number of total calls to the method. incremented before the actual method call.",
},
[]string{"method"},
),
CallErrorsTotal: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "call_errors_total",
Help: "the number of total errors returned from the method. incremented after the method call is ended.",
},
[]string{"method"},
),
}
}

func (this *PrometheusInterceptor) RegisterTo(registerer prometheus.Registerer) {
registerer.Register(this.CallDurations)
registerer.Register(this.CallsTotal)
registerer.Register(this.CallErrorsTotal)
}

func canReturnError(method reflect.Value) bool {
if method.Type().NumOut() == 0 {
return false
}
lastReturnType := method.Type().Out(method.Type().NumOut() - 1)
return lastReturnType.Name() == "error" && lastReturnType.PkgPath() == ""
}

func (this *PrometheusInterceptor) Intercept(receiver interface{}, method string, args []interface{}, delegate func([]interface{}) []interface{}) []interface{} {
r := reflect.ValueOf(receiver)
m := r.MethodByName(method)

this.CallsTotal.WithLabelValues(method).Inc()

t0 := time.Now()

rets := delegate(args)

if canReturnError(m) && rets[m.Type().NumOut()-1] != nil {
this.CallErrorsTotal.WithLabelValues(method).Inc()
}

seconds := time.Since(t0).Seconds()

this.CallDurations.WithLabelValues(method).Observe(seconds)

return rets
}

// Greeter

type GreeterImpl struct {
Random *rand.Rand
}

func (this *GreeterImpl) SayHello(name string) (string, error) {
nanos := this.Random.Float32() * float32(time.Second.Nanoseconds())
time.Sleep(time.Duration(nanos * float32(time.Nanosecond)))
if this.Random.Float32() < 0.5 {
return "", fmt.Errorf("failed to say hello")
}
return fmt.Sprintf("Hello %s!", name), nil
}

func main() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))

prom := NewPrometheusInterceptor("greeter")
prom.RegisterTo(prometheus.DefaultRegisterer)

p := &greeter.GreeterProxy{
Handler: &interceptor.InterceptingInvocationHandler{
Delegate: &GreeterImpl{Random: r},
Interceptor: prom,
},
}

go func() {
for {
msg, err := p.SayHello("James")
fmt.Println(msg, err)
time.Sleep(1 * time.Second)
}
}()

http.Handle("/metrics", promhttp.HandlerFor(
prometheus.DefaultGatherer,
promhttp.HandlerOpts{
EnableOpenMetrics: true,
},
))
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
}
```

```console
$ go run .
failed to say hello
Hello James!
failed to say hello
failed to say hello
Hello James!
...
```

```console
$ curl -s localhost:8080/metrics | grep greeter
# HELP greeter_call_duration_seconds time took to complete the method call
# TYPE greeter_call_duration_seconds histogram
greeter_call_duration_seconds_bucket{method="SayHello",le="0.005"} 0
greeter_call_duration_seconds_bucket{method="SayHello",le="0.01"} 0
greeter_call_duration_seconds_bucket{method="SayHello",le="0.025"} 0
greeter_call_duration_seconds_bucket{method="SayHello",le="0.05"} 0
greeter_call_duration_seconds_bucket{method="SayHello",le="0.1"} 0
greeter_call_duration_seconds_bucket{method="SayHello",le="0.25"} 2
greeter_call_duration_seconds_bucket{method="SayHello",le="0.5"} 9
greeter_call_duration_seconds_bucket{method="SayHello",le="1"} 24
greeter_call_duration_seconds_bucket{method="SayHello",le="2.5"} 24
greeter_call_duration_seconds_bucket{method="SayHello",le="5"} 24
greeter_call_duration_seconds_bucket{method="SayHello",le="10"} 24
greeter_call_duration_seconds_bucket{method="SayHello",le="+Inf"} 24
greeter_call_duration_seconds_sum{method="SayHello"} 14.092818702
greeter_call_duration_seconds_count{method="SayHello"} 24
# HELP greeter_call_errors_total the number of total errors returned from the method. incremented after the method call is ended.
# TYPE greeter_call_errors_total counter
greeter_call_errors_total{method="SayHello"} 11
# HELP greeter_calls_total the number of total calls to the method. incremented before the actual method call.
# TYPE greeter_calls_total counter
greeter_calls_total{method="SayHello"} 24
```

#### Ideas

* Memoizing / Caching

* Access Control

If you came up with an interesting idea not listed here, share with us by filing an issue or submitting a pull request!