Package call is a small wrapper around the official reflect package that eases dynamic function or method calls.
call can be useful for rigging routes to handlers on Go types in a dynamic fashion. An example of that will follow but let's first see some examples of call in action.
fn := func(str string, num int) {
fmt.Printf("str=%v num=%v\n", str, num)
}
f := call.StatFunc(fn)
f.Call(f.Args())
// prints:
// str= num=0The call to StatFunc returns a Func type that can be used to create function arguments and then invoke the function as seen by f.Call(f.Args()).
When Args() creates arguments it creates zero values for the argument types.
The previous example is somewhat useless because the function is called with a zero-value for each argument. Now we'll set the argument values using the Pointers field of Args:
// Same function as before.
fn := func(str string, num int) {
fmt.Printf("str=%v num=%v\n", str, num)
}
f := call.StatFunc(fn)
// Args contains two slices that give us access to the created arguments.
args := f.Args()
for k := range args.Values {
// args.Pointers are pointers to the arguments.
pointer := args.Pointers[k]
switch p := pointer.(type) {
case *string:
*p = "Hi!"
case *int:
*p = 42
}
// args.Values are reflect.Value of the argument.
value := args.Values[k].Interface()
fmt.Printf("%T %v %T\n", value, value, pointer)
}
f.Call(args)
// prints:
// string Hi! *string
// int 42 *int
// str=Hi! num=42The Pointers field is also useful for unmarshaling data into function arguments:
type Request struct {
Str string `json:"str"`
Num int `json:"num"`
}
fn := func(req Request) {
fmt.Printf("str=%v num=%v\n", req.Str, req.Num)
}
data := []byte(`{"str" : "Hi!", "num" : 42}`)
f := call.StatFunc(fn)
args := f.Args()
// For brevity we unmarshal straight into args.Pointers[0]
if err := json.Unmarshal(data, args.Pointers[0]); err != nil {
fmt.Println(err)
return
}
f.Call(args)
// prints:
// str=Hi! num=42When an argument is an interface I its value is I(nil) and its pointer is also nil:
// Interfaces are always passed as nil.
fn := func(w http.ResponseWriter) {
fmt.Println(w)
}
f := call.StatFunc(fn)
args := f.Args()
// When an argument represents an interface I its Values is an I(nil)
// and its Pointers is nil.
fmt.Println(args.Values[0].Interface(), args.Pointers[0])
f.Call(args)
// prints:
// <nil> <nil>
// <nil>Since interface types are provided as nil values by Args() you may wish to configure the *Func to stop managing such types. You do this by calling PruneIn(), which accepts a variadic list of reflect.Type:
// In order to prune a type we need its reflect.Type. Let's pretend we're writing
// a more general purpose http.Handler and want to prune http.ResponseWriter
// and *http.Request from types created via `Args()`:
TypeRequest := reflect.TypeOf((*http.Request)(nil))
// This is the idomatic way to get the type of a nil interface.
TypeResponseWriter := reflect.TypeOf((*http.ResponseWriter)(nil)).Elem()
fn := func( w http.ResponseWriter, req *http.Request, req SomeStructType ) {
}
// We're going to take the standard http.Handler signature and dynamically invoke fn
handler := func( w http.ResponseWriter, req *http.Request ) {
f := call.StatFunc(fn)
pruned := f.PruneIn(TypeRequest, TypeResponseWriter)
args := f.Args()
//
// Before invoking f we should see if we can provide any pruned arguments:
for _, arg := range pruned {
switch arg.T {
case TypeRequest:
args.Values[arg.N] = reflect.ValueOf(req)
case TypeResponseWriter:
args.Values[arg.N] = reflect.ValueOf(w)
}
}
//
// NB A more useful handler would potentially unmarshal req.Body
// into args.Pointers that could accept it.
//
f.Call(args)
}Let's take some ideas from the previous snippet and create a http.Handler factory:
TypeRequest := reflect.TypeOf((*http.Request)(nil))
TypeResponseWriter := reflect.TypeOf((*http.ResponseWriter)(nil)).Elem()
// Factory accepts a function and turns it into an http.Handler.
Factory := func(opaque interface{}) http.Handler {
f := call.StatFunc(opaque)
pruned := f.PruneIn(TypeRequest, TypeResponseWriter)
//
// The created handler does not represent a "production-ready" http.Handler but it does
// demonstrate how "package call" can be used to:
// + invoke end-of-chain handlers with adhoc or variable signatures
// + how to unmarshal and provide data to the handler arguments
// (by unmarshaling application/json requests, for example)
fn := func(w http.ResponseWriter, req *http.Request) {
args := f.Args()
// Before invoking f we should see if we can provide any pruned arguments:
for _, arg := range pruned {
switch arg.T {
case TypeRequest:
args.Values[arg.N] = reflect.ValueOf(req)
case TypeResponseWriter:
args.Values[arg.N] = reflect.ValueOf(w)
}
}
//
// If the request is application/json we will unmarshal into any arguments
// that are struct.
// NB: An intelligent handler factory would have examined f.InCreate and possibly set a
// hasJSON=true|false flag and could theoretically skip this logic block if the
// end-of-chain handler doesn't have targets for JSON data.
if req.Header.Get("Content-Type") == "application/json" {
body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, arg := range f.InCreate {
if arg.T.Kind() != reflect.Struct {
continue
}
if err = json.Unmarshal(body, args.Pointers[arg.N]); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
//
// NB: This handler doesn't do anything with any return values. A better handler
// factory would probably make use of any error returned or possibly accept
// some type of result and then write to the response appropriately.
f.Call(args)
}
return http.HandlerFunc(fn)
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
Login := func(w http.ResponseWriter, post LoginRequest) {
fmt.Fprintf(w, "%v", post)
}
Logout := func(w http.ResponseWriter) {
fmt.Fprint(w, "Logged out!")
}
mux := http.NewServeMux()
mux.Handle("/login", Factory(Login))
mux.Handle("/logout", Factory(Logout))
// /login
w := httptest.NewRecorder()
w.Body = &bytes.Buffer{}
req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBufferString(`{"username":"test","password":"s3cr3t"}`))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(w, req)
fmt.Println(w.Body.String())
// /logout
w = httptest.NewRecorder()
w.Body = &bytes.Buffer{}
req = httptest.NewRequest(http.MethodPost, "/logout", nil)
mux.ServeHTTP(w, req)
fmt.Println(w.Body.String())
// prints:
// {test s3cr3t}
// Logged out!I am making a very concerted effort to break the API as little as possible while adding features or fixing bugs. However this software is currently in a pre-1.0.0 version and breaking changes are allowed under standard semver. As the API approaches a stable 1.0.0 release I will list any such breaking changes here and they will always be signaled by a bump in minor version.
- 0.1.x ⭢ 0.2.0
- Several types have been renamed to be more ergonomic:
Methodsrenamed toInstanceMethodInforenamed toMethodMethodResultrenamed toResult- Fields
InCacheArgsandInCreateArgshave had theArgssuffix dropped and are now simplyInCacheandInCreate. Receivertype dropped entirely; theRebind()function now exists onInstance.
callnow supports invoking methods on types or regular functions. To support this a new typeFunchas been introduced.Funcwas created by pulling several fields out ofMethod(previouslyMethodInfo).Methodretains access to this extracted information by embedding*Func; in other wordsFuncis for calling regular functions,Methodis for calling functions that have receivers, andMethodis a superset ofFunc.- Added a new
type Methods []Methodwhich has a helper function for finding a method by name; note that thisMethodstype is not the same nor is it compatible withMethodstype in the previous release.
- Several types have been renamed to be more ergonomic: