• This repository has been archived on 06/Dec/2019
  • Stars
    star
    330
  • Rank 127,657 (Top 3 %)
  • Language
    Go
  • License
    MIT License
  • Created about 11 years ago
  • Updated over 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Make Go packages extensible

go-extpoints

This Go generator, named short for "extension points", provides a generic inversion of control model for making extensible Go packages, libraries, and applications.

It generates package extension point singletons from extension types you define. Extension points are then used to both register extensions and use the registered extensions with a common meta-API.

Logspout is a real application built using go-extpoints. Read about it here.

Getting the tool

$ go install github.com/progrium/go-extpoints

Concepts

Extension Types

These define your hooks. They can be Go interfaces or simple function signature types. Here are some generic examples:

type ConfigStore interface {
	Get(key string) (string, error)
	Set(key, value string) error
	Del(key string) error
}

type AuthProvider interface {
	Authenticate(user, pass string) bool
}

type EventListener interface {
	Notify(event Event)
}

type HttpEndpoint func() http.Handler

type RequestModifier func(request *http.Request)

Extension Points

With types defined, go-extpoints generates package singletons for each type. When your program starts, extensions are registered with extension points. Then you can then use registered extensions in a number of ways:

// Lookup a single registered extension for drivers
config := ConfigStores.Lookup(configStore)
if config == nil {
	log.Fatalf("config store '%s' not registered", configStore)
}
config.Set("foo", "bar")

// Iterate until you get what you need
func authenticate(user, pass string) bool {
	for _, provider := range AuthProviders.All() {
		if provide.Authenticate(user, pass) {
			return true
		}
	}
	return false
}

// Fire and forget events to all extensions
for _, listener := range EventListeners.All() {
	listener.Notify(event)
}

// Use name and return value of all extensions for registration
for name, handler := range HttpEndpoints.All() {
	http.Handle("/"+name, handler())
}

// Pass by reference to all extensions for middleware
for _, modifier := range RequestModifiers.All() {
	modifier(req)
}

Extension Point API

All extension types passed to go-extpoints will be turned into extension point singletons, using the pluralized name of the extension type. These extension point objects implement this simple meta-API:

type <ExtensionPoint> interface {
	// if name is "", the specific extension type is used.
	// returns false if doesn't implement type or already registered.
	Register(extension <ExtensionType>, name string) bool

	// returns false if not registered to start with
	Unregister(name string) bool

	// returns nil if not registered
	Lookup(name string) <ExtensionType>

	// for sorted subsets. each name is looked up in order, nil or not
	Select(names []string) []<ExtensionType>

	// all registered, keyed by name
	All() map[string]<ExtensionType>

	// convenient list of names
	Names() []string

}

It also generates top-level registration functions that will run extensions through all known extension points, registering or unregistering with any that are based on an interface the extension implements. They return the names of the interfaces they were registered/unregistered with.

func RegisterExtension(extension interface{}, name string) []string

func UnregisterExtension(name string) []string

Example Application

Here is a full Go application that lets extensions hook into main() as subcommands simply by implementing an interface we'll make called Subcommand. This interface will have just one method Run(), but you can make extension points based on any interface.

Assuming our package lives under $GOPATH/src/github.com/quick/example, here is our main.go:

//go:generate go-extpoints
package main

import (
	"fmt"
	"os"

	"github.com/quick/example/extpoints"
)

var subcommands = extpoints.Subcommands

func usage() {
	fmt.Println("Available commands:\n")
	for name, _ := range subcommands.All() {
		fmt.Println(" - ", name)
	}
	os.Exit(2)
}

func main() {
	if len(os.Args) < 2 {
		usage()
	}
	cmd := subcommands.Lookup(os.Args[1])
	if cmd == nil {
		usage()
	}
	cmd.Run(os.Args[2:])
}

Two things to note. First, the go:generate directive at the top. This tells go generate it needs to run go-extpoints, which will happen in a moment.

Another thing to note, the extension point is accessed by a variable named by the plural of our interface Subcommand and in this case lives under a separate extpoints subpackage.

We need to create that subpackage with a Go file in it to define our interface used for this extension point. This is our extpoints/interfaces.go:

package extpoints

type Subcommand interface {
	Run(args []string)
}

We use go generate now to produce the extension point code in our extpoints subpackage. These extension points are based on any Go interfaces you've defined in there. In our case, just Subcommand.

$ go generate
 ...
$ go install

Okay, but it doesn't do anything! Let's make a builtin command extension that implements Subcommand. Add a hello.go file:

package main

import (
	"fmt"
	"github.com/quick/example/extpoints"
)

func init() {
	extpoints.Register(new(HelloComponent), "hello")
}

type HelloComponent struct {}

func (p *HelloComponent) Run(args []string) {
	fmt.Println("Hello world!")
}

Now when we build and run the app, it shows hello as a subcommand. We've registered the component with the name hello, which we happen to use to identify the name of the subcommand. Component names are optional, but can be handy for situations like this one.

Certainly, the value of extension points becomes clearer with larger applications and more interesting interfaces. But just consider now that the component defined in hello.go could exist in another package in another repo. You'd just have to import it and rebuild to let it hook into our application.

There are two more in-deptch example applications in this repo to take a look at:

  • tool (extpoints), a more realistic CLI tool with subcommands and lifecycle hooks
  • daemon, ... doesn't exist yet

Making it easy to install extensions

Assuming you tell third-party developers to call your package or extension point Register in their init(), you can link them with a side-effect import (using a blank import name).

You can make this easy for users to enable/disable via comments, or add their own without worrying about messing with your code by having a separate extensions.go or plugins.go file with just these imports:

package yourpackage

import (
	_ "github.com/you/some-extension"
	_ "github.com/third-party/another-extension"
)

Users can now just edit this file and go build or go install.

Usage Patterns

Here are different example ways to use extension points to interact with extensions:

Simple Iteration

for _, listener := range extpoints.EventListeners.All() {
	listener.Notify(&MyEvent{})
}

Lookup Only One

driverName := config.Get("storage-driver")
driver := extpoints.StorageDrivers.Lookup(driverName)
if driver == nil {
	log.Fatalf("storage driver '%s' not installed", driverName)
}
driver.StoreObject(object)

Passing by Reference

for _, filter := range extpoints.RequestFilters.All() {
	filter.FilterRequest(req)
}

Match and Use

for _, handler := range extpoints.RequestHandlers.All() {
	if handler.MatchRequest(req) {
		handler.HandleRequest(req)
		break
	}
}

Why the extpoints subpackage?

Since we encourage the convention of a subpackage called extpoints, it makes it very easy to identify a package as having extension points from looking at the project tree. You then know where to look to find the interfaces that are exposed as extension points.

Third-party packages have a well known package to import for registering. Whether you have extension points for a library package or a command with just a main package, there's always a definite extpoints package there to import.

It also makes it clearer in your code when you're using extension points. You have to explicitly import the package, then call extpoints.<ExtensionPoint> when using them. This helps identify where extension points actually hook into your program.

Groundwork for Dynamic Extensions

Although this only seems to allow for compile-time extensibility, this itself is quite a win. It means power users can build and compile in their own extensions that live outside your repository.

However, it also lays the groundwork for other dynamic extensions. I've used this model to wrap extension points for components in embedded scripting languages, as hook scripts, as remote plugin daemons via RPC, or all of the above implemented as components themselves!

No matter how you're thinking about dynamic extensions later on, using go-extpoints gives you a lot of options. Once Go supports dynamic libraries? This will work perfectly with that, too.

Inspiration

This project and component model is a lightweight, Go idiomatic port of the component architecture used in Trac, which is written in Python. It's taken about a year to get this right in Go.

Trac Component Architecture

License

BSD

More Repositories

1

darwinkit

Native Mac APIs for Go. Previously known as MacDriver
Go
5,002
star
2

localtunnel

Expose localhost servers to the Internet
Go
3,180
star
3

bashstyle

Let's do Bash right!
1,791
star
4

gitreceive

Easily accept and handle arbitrary git pushes
Shell
1,141
star
5

buildstep

Buildstep uses Docker and Buildpacks to build applications like Heroku
Groovy
908
star
6

entrykit

Entrypoint tools for elegant, programmable containers
Go
442
star
7

keychain.io

Python
395
star
8

duplex

Full duplex modern RPC
Python
385
star
9

busybox

Busybox container with glibc+opkg
Shell
384
star
10

go-basher

Library for writing hybrid Go and Bash programs
Go
375
star
11

topframe

Local webpage screen overlay for customizing your computing experience
JavaScript
349
star
12

ginkgo

Python service microframework
Python
323
star
13

envy

Lightweight dev environments with a twist
JavaScript
321
star
14

termshare

Quick and easy terminal sharing.
Go
320
star
15

go-shell

Go
311
star
16

skypipe

A magic pipe in the sky for the command line
Python
307
star
17

nullmq

ZeroMQ-like sockets in the browser. Used for building gateways and generally applying ZeroMQ philosophy to browser messaging.
JavaScript
276
star
18

wssh

wssh ("wish") is a command-line utility/shell for WebSocket inspired by netcat
Python
260
star
19

viewdocs

Read the Docs meets Gist.io for simple Markdown project documentation
Go
257
star
20

qmux

wire protocol for multiplexing connections or streams into a single connection, based on a subset of the SSH Connection Protocol
Go
231
star
21

docker-stress

Docker container for generating workload stress
Dockerfile
221
star
22

nginx-appliance

A programmable Nginx container
Shell
199
star
23

pluginhook

Simple dispatcher and protocol for shell-based plugins, an improvement to hook scripts
Go
180
star
24

hookah

Asynchronous HTTP request dispatcher for webhooks
Python
144
star
25

cedarish

Heroku Cedar-ish Base Image for Docker
Shell
116
star
26

gh-release

DEPRECATED -- Utility for automating Github releases with file uploads
Shell
112
star
27

notify-io

Open notification platform for the web
Python
107
star
28

postbin

Webhook data inspector
Python
106
star
29

docker-plugins

Plugins for Docker
Shell
102
star
30

basht

Minimalist Bash test runner
Go
98
star
31

embassy

Easy, distributed discovery and routing mesh for Docker powered by Consul
Shell
94
star
32

configurator

Go
89
star
33

scriptlets

Web scripting in the cloud
JavaScript
64
star
34

raiden

Python
60
star
35

hotweb

Live reloading and ES6 hot module replacement for plain old JavaScript
Go
56
star
36

http-subscriptions

54
star
37

rootbuilder

Base Docker image for using buildroot to produce a rootfs.tar
Makefile
53
star
38

miyamoto

Python
45
star
39

oauth2-appengine

Reference server implementation for OAuth2 that runs on App Engine
Python
44
star
40

buildpack-nginx

nginx buildpack
Shell
42
star
41

DarkForest

C#
41
star
42

qtalk-go

versatile stream IO and RPC based IPC stack for Go
Go
41
star
43

systembits

Simplest profiler ever. Like ohai but just shell scripts.
Shell
40
star
44

DrEval

JavaScript sandbox (eval) as a service
Python
39
star
45

pydoozer

Python client for Doozer using gevent
Python
37
star
46

ginkgotutorial

Python
37
star
47

skywatch

Magic cloud alerting system in a self-contained command-line utility
Ruby
37
star
48

yapper

A Jabber/XMPP interface to Growl
Python
34
star
49

wolverine

Previously Miyamoto, a Twisted hub implementation of PubSubHubbub
Python
33
star
50

dockerhook

Docker event stream listener that triggers a hook script
Go
33
star
51

protocol-droid

Universal (read: HTTP) protocol bridge
Python
28
star
52

clon-spec

Command-Line Object Notation: Ergonomic JSON-compatible input syntax for CLI tools.
22
star
53

wsio

Pipe data anywhere
Ruby
22
star
54

hostpool

A worker pool manager for DigitalOcean hosts.
Go
22
star
55

shelldriver

Go
19
star
56

prototypes

Collection of experiments and prototypes
Go
19
star
57

macschema

Toolchain for generating JSON definitions of Apple APIs
Go
18
star
58

vizgo

a visual golang software editor
JavaScript
17
star
59

gh-pages-auth

Set up GitHub Pages and Auth0 authentication with minimal effort
HTML
15
star
60

consul-access

Nginx
14
star
61

mailhooks

Get Email as HTTP POST
Python
13
star
62

busybox-docker

Minimal Docker image with the Docker binary
Shell
13
star
63

tview-ssh

Example using tcell+tview over SSH using gliderlabs/ssh
Go
13
star
64

webdns

DNS over HTTP. Serve DNS with a REST API
12
star
65

irc-for-gmail

Embeddable IRC client for Gmail via Chrome extension. EXPERIMENTAL
JavaScript
11
star
66

ohai-there

Easy system profiling in a Docker container
Go
11
star
67

go-plugins-lua

Lua runtime for go-plugins
Go
11
star
68

dockerbuilder

Shell
10
star
69

javascriptd

Node.js powered script execution container
JavaScript
10
star
70

httpmail

A REST/Atom gateway to IMAP
10
star
71

docker-releasetag

Shell
10
star
72

go-streamkit

High level stream plumbing API in Go
Go
9
star
73

sveltish

Go
9
star
74

progrium.com

My website
HTML
9
star
75

gh-download

Proxy to latest Github Release asset download
Go
9
star
76

go-scripting

Go
9
star
77

tracker-widget

Pivotal Tracker widget for listing stories
Python
9
star
78

cometcatchr

An opinionated Comet client in Flash for Javascript
JavaScript
9
star
79

hackerdojo-signin

Python
8
star
80

pubsubhubbub-testsuite

Hub validation of the PubSubHubbub spec
Ruby
8
star
81

hd-events

This repo is no longer canonical! See link below:
Python
8
star
82

simplex

Go
8
star
83

groknet

ngrok as a net.Listener
Go
8
star
84

growl

A mirror of Growl from Mercurial
Objective-C
8
star
85

docker-9p

Docker Volume Plugin for 9P
Go
8
star
86

websocket-radio

JavaScript
8
star
87

dockerhub-tag

Go
7
star
88

registrator

I hate Docker Hub
7
star
89

domfo

Simple domain forwarder -- redirects web requests based on URL in TXT record
Python
7
star
90

docker-plugin

docker plugin subcommand UX prototype
Shell
7
star
91

electron-largetype

Large Type for Electron apps
HTML
7
star
92

jabberhooks

Jabber to webhook service
Python
6
star
93

platformer

Go
6
star
94

webhooks

Website for webhooks.org
HTML
6
star
95

stomp4py

Python
6
star
96

hackerdojo-signup

The Hacker Dojo member signup app
Python
6
star
97

hackerdojo-kiosk

JavaScript
6
star
98

domdori

Domains Done Right
Python
6
star
99

goja-automerge

Automerge.js in Go via goja
JavaScript
6
star
100

usb

universal seinfeld binary
Go
6
star