Go create a mock for gcp compute sdk - go

I use the following function, and I need to raise the coverage of it (if possible to 100%), the problem is that typically I use interface to handle such cases in Go and for this specific case not sure how to do it, as this is a bit more tricky, any idea?
The package https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/compute/v1
Which I use doesn't have interface so not sure how can I mock it?
import (
"context"
"errors"
"fmt"
"os"
compute "cloud.google.com/go/compute/apiv1"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
computev1 "google.golang.org/genproto/googleapis/cloud/compute/v1"
)
func Res(ctx context.Context, project string, region string,vpc string,secret string) error {
c, err := compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))
if err != nil {
return err
}
defer c.Close()
addrReq := &computev1.ListAddressesRequest{
Project: project,
Region: region,
}
it := c.List(ctx, addrReq)
for {
resp, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
if *(resp.Status) != "IN_USE" {
return ipConverter(*resp.Name, vpc)
}
}
return nil
}

Whenever I find myself in this scenario, I found that the easiest solution is to create missing interfaces myself. I limit these interfaces to the types and functions that I am using, instead of writing interfaces for the entire library. Then, in my code, instead of accepting third-party concrete types, I accept my interfaces for those types. Then I use gomock to generate mocks for these interfaces as usual.
The following is a descriptive example inspired by your code.
type RestClient interface {
List(context.Context, *computev1.ListAddressesRequest) (ListResult, error) // assuming List returns ListResult type.
Close() error
}
func newRestClient(ctx context.Context, secret string) (RestClient, error) {
return compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))
}
func Res(ctx context.Context, project string, region string, vpc string, secret string) error {
c, err := newRestClient(ctx, secret)
if err != nil {
return err
}
defer c.Close()
return res(ctx, project, region, vpc, c)
}
func res(ctx context.Context, project string, region string, vpc string, c RestClient) error {
addrReq := &computev1.ListAddressesRequest{
Project: project,
Region: region,
}
it, err := c.List(ctx, addrReq)
if err != nil {
return err
}
for {
resp, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
if *(resp.Status) != "IN_USE" {
return ipConverter(*resp.Name, vpc)
}
}
return nil
}
Now you can test the important bits of the Res function by injecting a mock RestClient to the internal res function.

One obstacle to testability here is that you instantiate a client inside your Res function rather than injecting it. Because
the secret doesn't change during the lifetime of the programme,
the methods of *compute.AddressesClient (other than Close) are concurrency-safe,
you could create one client and reuse it for each invocation or Res. To inject it into Res, you can declare some Compute type and turn Res into a method on that type:
type Compute struct {
Lister Lister // some appropriate interface type
}
func (cp *Compute) Res(ctx context.Context, project, region, vpc string) error {
addrReq := &computev1.ListAddressesRequest{
Project: project,
Region: region,
}
it := cp.Lister.List(ctx, addrReq)
for {
resp, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
if *(resp.Status) != "IN_USE" {
return ipConverter(*resp.Name, vpc)
}
}
return nil
}
One question remains: how should you declare Lister? One possibility is
type Lister interface {
List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) *compute.AddressIterator
}
However, because compute.AddressIterator is a struct type with some unexported fields and for which package compute provides no factory function, you can't easily control how the iterator returned from List behaves in your tests. One way out is to declare an additional interface,
type Iterator interface {
Next() (*computev1.Address, error)
}
and change the result type of List from *compute.AddressIterator to Iterator:
type Lister interface {
List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator
}
Then you can declare another struct type for the real Lister and use that on the production side:
type RealLister struct {
Client *compute.AddressesClient
}
func (rl *RealLister) List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator {
return rl.Client.List(ctx, req, opts...)
}
func main() {
secret := "don't hardcode me"
ctx, cancel := context.WithCancel(context.Background()) // for instance
defer cancel()
c, err := compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))
if err != nil {
log.Fatal(err) // or deal with the error in some way
}
defer c.Close()
cp := Compute{Lister: &RealLister{Client: c}}
if err := cp.Res(ctx, "my-project", "us-east-1", "my-vpc"); err != nil {
log.Fatal(err) // or deal with the error in some way
}
}
For your tests, you can declare another struct type that will act as a configurable test double:
type FakeLister func(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator
func (fl FakeLister) List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator {
return fl(ctx, req, opts...)
}
To control the behaviour of the Iterator in your test, you can declare another configurable concrete type:
type FakeIterator struct{
Err error
Status string
}
func (fi *FakeIterator) Next() (*computev1.Address, error) {
addr := computev1.Address{Status: &fi.Status}
return &addr, fi.Err
}
A test function may look like this:
func TestResStatusInUse(t *testing.T) {
// Arrange
l := func(_ context.Context, _ *computev1.ListAddressesRequest, _ ...gax.CallOption) Iterator {
return &FakeIterator{
Status: "IN_USE",
Err: nil,
}
}
cp := Compute{Lister: FakeLister(l)}
dummyCtx := context.Background()
// Act
err := cp.Res(dummyCtx, "my-project", "us-east-1", "my-vpc")
// Assert
if err != nil {
// ...
}
}

Related

Maximize the number of CustomResources that a CustomResourceDefinition can have | kubebuilder & operator-sdk

I'm developing a kubernetes operator that represents a very simple api and a controller.
I would like to maximize the number of the CustomResources those could belonging to the specific CustomResourceDefinition that the operator defines. (As specially I would like to allow just one CR, if it is already defined, the operator should throw an error message and skip reconciling it.)
If I generate the api, there is a KindList struct default generated, and if I understand correctly, it should keep track of the CRs already defined for my CRD. It is also added to the scheme by default. See the example from kubebuilder documentation:
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// CronJob is the Schema for the cronjobs API
type CronJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CronJobSpec `json:"spec,omitempty"`
Status CronJobStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// CronJobList contains a list of CronJob
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CronJob `json:"items"`
}
func init() {
SchemeBuilder.Register(&CronJob{}, &CronJobList{})
}
Unfortunately, I can not find out how to access this List from the controller. I have tried like this, but r.Get can not accept cacheList:
cronjob/cronjob_controller.go
package controllers
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
cronjobv1alpha1 "github.com/example/cronjob-operator/api/v1alpha1"
)
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
cronjob := cronjobv1alpha1.Memcached{}
if err := r.Get(ctx, req.NamespacedName, &cronjob); err != nil {
return ctrl.Result{}, err
}
cronjobList := cachev1alpha1.MemcachedList{}
if err := r.Get(ctx, req.NamespacedName, &cronjobList); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
If I get the list, I could validate the length of it, and do or skip the reconcile.
Is it even a correct approach? Is there a better way to achieve my goal? Should I create a webhook instead?
Assuming you are using the default sigs.k8s.io/controller-runtime/pkg/client's client.Client, you get access to the List() function.
In your case r.List(...).
Usage:
case 1: list by label
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
cronjobList := cronjobv1alpha1.CronJobList{}
err = r.List(ctx, &cronjobList, client.MatchingLabels{"foo": "bar"})
if err != nil {
return ctrl.Result{}, err
}
}
case 2: list all in namespace
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
cronjobList := cronjobv1alpha1.CronJobList{}
err = r.List(ctx, &cronjobList, client.InNamespace("default"))
if err != nil {
return ctrl.Result{}, err
}
}
case 3: list by field i.e. metadata.name
// in your Reconciler Setup function create an index
func SetupWithManager(mgr ctrl.Manager) error {
r := &CronJobReconciler{
Client: mgr.GetClient(),
}
mgr.GetFieldIndexer().IndexField(context.TODO(), &cronjobv1alpha1.CronJob{}, "metadata.name", NameIndexer)
return ctrl.NewControllerManagedBy(mgr).
For(&cronjobv1alpha1.CronJob{}).
Complete(r)
}
func NameIndexer(o client.Object) []string {
m := o.(*cronjobv1alpha1.CronJob)
return []string{m.ObjectMeta.Name}
}
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
cronjobList := cronjobv1alpha1.CronJobList{}
err = r.List(ctx, &cronjobList, client.MatchingFields{"metadata.name": "test"})
if err != nil {
return ctrl.Result{}, err
}
}

How to unit test code consuming gcloud storage?

I want to write unit test for the below code
package main
import (
"context"
"google.golang.org/api/option"
"cloud.google.com/go/storage"
)
var (
NewClient = storage.NewClient
)
func InitializeClient(ctx context.Context) (*storage.Client, error) {
credFilePath := "Storage credentials path."
// Creates a client.
client, err := NewClient(ctx, option.WithCredentialsFile(credFilePath))
if err != nil {
return nil, err
}
return client, nil
}
func createStorageBucket(ctx context.Context, client *storage.Client, bucketName string) (*storage.BucketHandle, error) {
// Sets your Google Cloud Platform project ID.
projectID := "Some project id"
// Creates a Bucket instance.
bucket := client.Bucket(bucketName)
// Creates the new bucket.
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
if err := bucket.Create(ctx, projectID, nil); err != nil {
return nil, err
}
return bucket, nil
}
func bucketExists(ctx context.Context, client *storage.Client, bucketName string) error {
bucket := client.Bucket(bucketName)
if _, err := bucket.Attrs(ctx); err != nil {
//try creating the bucket
if _, err := createStorageBucket(ctx, client, bucketName); err != nil {
return err
}
}
return nil
}
func main() {
ctx = context.Background()
client, err := InitializeClient(ctx)
bucketName := "Some bucket name"
err = bucketExists(ctx, client, bucketName)
}
bucket.Create() and bucket.Attrs() are http calls, also Bucket(), Object() and NewReader() returning structs(So in my sense there is no meaning of implement interface for this use case)
Note: storage.NewClient() is also http call but i am avoiding external call using monkey pathching approch in my test by providing custom implementaion.
var (
NewClient = storage.NewClient
)
The code is so thin that is is hard to figure how to test that.
I guess Flimzy grasped that reading at the title.
There is a fundamental misunderstanding because of/in the title How to write unit test for gcloud storage?.
Well we should not. They did it. A better title would be How to unit test code consuming gcloud storage? I am not trying to be picky here, but explains what i understood trying to solve that question.
So anyways, this whole thing lead me to write less thin code, so that i can test that the code i write, the lines driving the storage, did what we expect it does.
This whole thing is so convoluted and out of tin air that I dont think it will answer your question.
but anyways, if that helps thinking about this difficulty that is already a win.
package main
import (
"context"
"flag"
"fmt"
"testing"
"time"
"cloud.google.com/go/storage"
"google.golang.org/api/option"
)
type client struct {
c *storage.Client
projectID string
}
func New() client {
return client{}
}
func (c *client) Initialize(ctx context.Context, projectID string) error {
credFilePath := "Storage credentials path."
x, err := NewClient(ctx, option.WithCredentialsFile(credFilePath))
if err == nil {
c.c = x
c.projectID = projectID
}
return err
}
func (c client) BucketExists(ctx context.Context, bucketName string) bool {
if c.c == nil {
return nil, fmt.Errorf("not initialized")
}
bucket := c.c.Bucket(bucketName)
err := bucket.Attrs(ctx)
return err == nil
}
func (c client) CreateBucket(ctx context.Context, bucketName string) (*storage.BucketHandle, error) {
if c.c == nil {
return nil, fmt.Errorf("not initialized")
}
bucket := c.c.Bucket(bucketName)
err := bucket.Create(ctx, c.projectID, nil)
if err != nil {
return nil, err
}
return bucket, err
}
func (c client) CreateBucketIfNone(ctx context.Context, bucketName string) (*storage.BucketHandle, error) {
if !c.BucketExists(bucketName) {
return c.CreateBucket(ctx, c.projectID, bucketName)
}
return c.c.Bucket(bucketName), nil
}
type clientStorageProvider interface { // give it a better name..
Initialize(ctx context.Context, projectID string) (err error)
CreateBucketIfNone(ctx context.Context, bucketName string) (*storage.BucketHandle, error)
CreateBucket(ctx context.Context, bucketName string) (*storage.BucketHandle, error)
BucketExists(ctx context.Context, bucketName string) bool
}
func main() {
flag.Parse()
cmd := flag.Arg(0)
projectID := flag.Arg(1)
bucketName := flag.Arg(2)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
client := New()
if cmd == "create" {
createBucket(ctx, client, projectID, bucketName)
} else {
// ... more
}
}
// this is the part we are going to test.
func createBucket(ctx context.Context, client clientStorageProvider, projectID, bucketName string) error {
err := client.Initialize(ctx, projectID)
if err != nil {
return err
}
return client.CreateBucketIfNone(bucketName)
// maybe we want to apply retry strategy here,
// and test that the retry was done;
}
type clientFaker struct {
initErr error
createErr error
createIfNoneErr error
bucketExistsErr error
}
func (c clientFaker) Initialize(ctx context.Context, projectID string) (err error) {
return c.initErr
}
func (c clientFaker) CreateBucketIfNone(ctx context.Context, bucketName string) (*storage.BucketHandle, error) {
return nil, c.createIfNoneErr
}
func (c clientFaker) CreateBucket(ctx context.Context, bucketName string) (*storage.BucketHandle, error) {
return nil, c.createErr
}
func (c clientFaker) BucketExists(ctx context.Context, bucketName string) bool {
return nil, c.bucketExistsErr
}
func TestCreateBucketWithFailedInit(t *testing.T) {
c := clientFaker{
initErr: fmt.Errorf("failed init"),
}
ctx := context.Background()
err := createBucket(ctx, c, "", "")
if err == nil {
t.Fatalf("should have failed to initialize the bucket")
}
}
// etc...
note that i am not happy having *storage.BucketHandle as a return parameter, too specific, but i had no use of it (i put it here because it was there, otherwise hanging), so it was hard to design something around that.
note², it might happen my code is not fully compilable. I am having a dependency problem that i don t want to fix now and it prevent me from seeing all errors (it stops too early in the process)

Test wrap function with dependency injection

I have this function which I need to mock in test,
I was able to mock it as expected with http mock package , but now I’ve function that are calling
To the HttpReq method and here I cannot use http mock package
I read about dependency injection and tried something but I wasn’t able to fully do it,
This is the function
type params struct {
cs string
ci string
method string
url string
}
// I added this struct but not sure if it's needed ... probably for test purpose but not sure how to use it.
type Impl struct {
client *http.Client
}
func (i *Impl) HttpReq(p *params) ([]byte, error) {
httpClient := i.client
req, err := http.NewRequest(p.method, p.url, nil)
if err != nil {
fmt.Sprintf(err)
}
req.SetBasicAuth(p.cs, p.ci)
res, err := httpClient.Do(req)
if err != nil {
fmt.Sprintf(err)
}
t, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Sprintf(err)
}
defer res.Body.Close()
return t, nil
}
This is what I tried
I’ve created interface
type Req interface {
HttpReq(params) ([]byte, error)
}
Now I’ve created a struct which contain the interface
type Service struct {
req Req
}
This is the function which I need to tests
func (c *Service) execute(cli Connection , args []string) (error, []byte) {
sk, err := c.doSomthing(cli, args)
sc, err := c.doSometing2(serviceK, []string{"url", "cl", "ct"})
cc := strings.Fields(serviceCredentials)
// ----------Here is what I need to mock ----------
t, err := c.req.HttpReq(params{cs: cc[1],
ci: cc[2],
method: http.MethodPost,
url: cc[0],})
return err, t
}
Any idea how I can run test for this function ??? Im struggling with it a lot.
Independent of the original question, you should not create new HTTP clients for each request. Client's maintain a connection pool and should be reused as much as possible.
You can fix that, and continue using your existing mock server by injecting the HTTP client.
Note also that the interface definition in the question doesn't match the implementation. These two method signatures are not the same:
HttpReq(params) ([]byte, error) // Req
HttpReq(*params) ([]byte, error) // Impl
Pick one. I would probably go with the non-pointer type here. And upper case initials are idiomatic in Go (HTTPReq, not HttpReq).
Add the client to the Impl type and use it in HTTPReq:
type Impl struct {
client *http.Client
}
func (i *Impl) HTTPReq(p params) ([]byte, error) {
req, err := http.NewRequest(p.method, p.url, nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(p.cs, p.ci)
res, err := i.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return ioutil.ReadAll(res.Body)
}
The Service type doesn't have to change.
In the tests, simply inject a test client into the Impl value:
import (
"context"
"net"
"net/http"
"net/http/httptest"
"testing"
)
func TestService_execute(t *testing.T) {
var testHandler http.Handler // TODO
srv := httptest.NewServer(testHandler)
defer srv.Close()
client := srv.Client()
tp := client.Transport.(*http.Transport)
// Direct all requests to the test server, no matter what the request URL is.
tp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
// Note that we ignore the network and addr parameters, since these are
// derived from the request URL and are unrelated to the test server.
srvAddr := srv.Listener.Addr()
return (&net.Dialer{}).DialContext(ctx, srvAddr.Network(), srvAddr.String())
}
svc := &Service{
req: &Impl{
client: client,
},
}
svc.execute(/* ... */)
// assert request, response, etc.
}
Since Service struct already has an req interface, during tests initialise service object with mock that satisfies req interface.
Something like this
https://stackoverflow.com/a/53805535/3968921

gomock, Go,mango package ,MongoMock

I am trying to mock the below method using gomock
func GetS(tenantName string) (*mgo.Session, error) {
ctx := apiContext.TContext{}
url, err := connectionURLList.get(tenantName)
if err != nil {
log.GenericWarning(ctx,
fmt.Sprintf("connection to %s not yet created, creating one: %v", tenantName, err), nil)
if err := connectMongo(tenantName); err == nil {
return GetS(tenantName) //singleton recursion to again call GetS
}
return nil, err
}
// ignoring error, expected we will always setting session in session map
session, _ := connectionList.get(url)
return session.Copy(), err
}
My Interface
type MongoManager interface {
GetS(tenantName string)
}
func TestGetS(t *testing.T) {
//var mgoCall *mgo.Session
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockManagerObj := mocks.NewMockMongoManager(mockCtrl)
mockManagerObj.EXPECT().GetS("cacargroup").Return(nil)
}
I am Getting the below error . Can someone help
$ go test
--- FAIL: TestGetS (0.00s)
mongoManager_test.go:20: missing call(s) to *mocks.MockMongoManager.GetS(is equal to cacargroup) /Users/charles/workspace/src/bitbucket.org/tekion/tbaas/mongoManager/mongoManager_test.go:16
mongoManager_test.go:20: aborting test due to missing call(s) FAIL exit status 1
You see actually the method in your interface implemented with return type of an error. But you are using like it returns nothing and chaining the implementation. Just remove the return type of GetS.
type fn func(string) (*mgo.Session, error)
type MongoManager interface {
NewFunction(GetS, "cascade")
}
func TestGetS(t *testing.T) {
//var mgoCall *mgo.Session
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockManagerObj := mocks.NewMockMongoManager(mockCtrl)
mockManagerObj.EXPECT().GetS("cacargroup").Return(nil)
}
Also you have to remove it from GetS function too
func NewFunction(GetS fn, value string){
GetS("cascade")
}
func GetS(tenantName string) (*mgo.Session, error){
ctx := apiContext.TContext{}
url, err := connectionURLList.get(tenantName)
if err != nil {
log.GenericWarning(ctx,
fmt.Sprintf("connection to %s not yet created, creating one: %v", tenantName, err), nil)
if err := connectMongo(tenantName); err == nil {
return GetS(tenantName) //singleton recursion to again call GetS
}
return nil, err
}
// ignoring error, expected we will always setting session in session map
session, _ := connectionList.get(url)
}

How to reduce repetitive http handler code in golang?

I'm designing a API server in Go. I have many database tables, each with a matching struct. Each has a route and handler:
type Thing1 struct {
ID int64
Name string
...
}
func main() {
...
router := mux.NewRouter()
apiRouter := router.PathPrefix("/v1").Subrouter()
apiRouter.HandleFunc("/thing1/{id}", Thing1ShowHandler).Methods("GET")
}
func Thing1ShowHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.ParseInt(vars["id"], 10, 64)
if err != nil {
errorHandler(w, err)
return
}
thing1 := Thing1{ID: id}
err = db.First(&thing1, id).Error
if thing1.ID > 0 {
jsonHeaders(w, http.StatusOK)
if err := json.NewEncoder(w).Encode(thing1); err != nil {
errorHandler(w, err)
}
return
}
notFoundHandler(w, r)
}
The code for Thing2 is pretty much identical, as it is for Thing3 and so on. I will end up with hundreds of things, and therefore lots of duplicated code. It feels like I'm doing something horribly wrong. What's the best way to make this more DRY?
Why not create a factory function for the http.Handler used with each Thing? This allows you to write the showHandler logic once and parameterize the instantiation of individual things.
// A ThingFactory returns a Thing struct configured with the given ID.
type ThingFactory func(id int64) interface{}
// The createShowHandler function is a factory function for creating a handler
// which uses the getThing factory function to obtain an instance of a
// thing to use when generating a view.
func createShowHandler(getThing ThingFactory) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.ParseInt(vars["id"], 10, 64)
if err != nil {
errorHandler(w, err)
return
}
thing := getThing(id)
err = db.First(&thing, id).Error
if err != nil {
errorHandler(w, err)
}
if thing1.ID > 0 {
jsonHeaders(w, http.StatusOK)
if err := json.NewEncoder(w).Encode(thing1); err != nil {
errorHandler(w, err)
}
return
}
notFoundHandler(w, r)
}
}
This function can be used to systematically create routes for a given router. For instance, I can create an explicit registry which keeps track of the path for each thing as well as a ThingFactory instance which is used when calling the createShowHandler factory function.
router := mux.NewRouter()
apiRouter := router.PathPrefix("/v1").Subrouter()
registry := []struct {
path string
handler ThingFactory
}{
{"/thing1/{id}", func(id int64) interface{} { return Thing1{ID: id} }},
{"/thing2/{id}", func(id int64) interface{} { return Thing2{ID: id} }},
{"/thing3/{id}", func(id int64) interface{} { return Thing3{ID: id} }},
}
for _, registrant := range registry {
apiRouter.HandleFunc(registrant.path, createShowHandler(registrant.handler)).Methods("GET")
}
Naturally, you would want to define interfaces for the various interaction points in a program like this to gain more type safety when dealing with a large number of instances. A more robust registry could be implemented that provided an interface for Things to register themselves with.

Resources