Skip to content

eiiches/go-gen-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 below). If you are familiar with Java, this is similar to Dynamic Proxy but with code generation.

Tutorial

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

    $ 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.

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

    You can also use a tool like ifacemaker to extract and generate an interface from existing structs.

  3. Generate a proxy struct that implements the interface.

    $ 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
    // 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: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?

    // +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.

    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)
    }
    $ go run ./cmd/tutorial
    Hello James! <nil>

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, 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
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)
}
$ go run .
ENTER: receiver = &{}, method = SayHello, args = [James]
EXIT: receiver = &{}, method = SayHello, args = [James], retvals = [Hello James! <nil>]
Hello James! <nil>
Fail a method call by some probability
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)
}
$ go run .
Hello James! <nil>
$ go run .
 injected error
Mock with Testify
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)
}
$ go run .
Hello James! <nil>
Instrument method calls and expose Prometheus metrics
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))
}
$ go run .
 failed to say hello
Hello James! <nil>
 failed to say hello
 failed to say hello
Hello James! <nil>
...
$ 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!