7, Go programming mode: modifier

Posted by blacksheepradio on Tue, 08 Feb 2022 03:10:01 +0100

I wrote an article before< Functional programming of Python modifier >, this mode can easily assemble some functions to other functions, which can make your code simpler, or make some "small function" code more reusable, so that the functions in the code can be assembled freely like LEGO toys. Therefore, I have always been fond of the programming mode of decorator decoration. Here is an article related to Go language.

Yes Python modifier The students of that article must know that this is a way of playing functional programming - wrap it with a high-order function. More nagging, about functional programming, you can refer to an article I wrote before< Functional programming >, the main purpose of this article is to drive more people to play functional programming through the transition from the thinking mode of procedural programming to the thinking mode of functional programming. Therefore, if you want to know about functional programming, you can read it step by step. Therefore, the modifier programming mode of Go language is actually the mode of functional programming.

However, it should be noted that the Go language does not have much "sugar" and is a strongly typed static virtual machine free language. Therefore, it is impossible to achieve the code of elegant modifiers like Java and Python. Of course, maybe I'm just a shallow student. If you know there are more ways to write, please tell me. Thank you first.

Simple example

Let's take a look at an example:

package main

import "fmt"

func decorator(f func(s string)) func(s string) {

    return func(s string) {
        fmt.Println("Started")
        f(s)
        fmt.Println("Done")
    }
}

func Hello(s string) {
    fmt.Println(s)
}

func main() {
        decorator(Hello)("Hello, World!")
}

We can see that we use a high-order function decorator(). When calling, we first pass in the Hello() function, and then it returns an anonymous function. In this anonymous function, in addition to running our own code, we also call the passed in Hello() function.

This method is similar to Python, but unfortunately, Go does not support @ decorator syntax like Python. So it's a little ugly on the call. Of course, if you want to make the code easier to read, you can do this:

hello := decorator(Hello)
hello("Hello")

Let's take another example of and calculating the running time:

package main

import (
  "fmt"
  "reflect"
  "runtime"
  "time"
)

type SumFunc func(int64, int64) int64

func getFunctionName(i interface{}) string {
  return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func timedSumFunc(f SumFunc) SumFunc {
  return func(start, end int64) int64 {

    defer func(t time.Time) {
      fmt.Printf("--- Time Elapsed (%s): %v ---\n", 
          getFunctionName(f), time.Since(t))
    }(time.Now())

    return f(start, end)
  }
}

func Sum1(start, end int64) int64 {
  var sum int64
  sum = 0
  if start > end {
    start, end = end, start
  }
  for i := start; i <= end; i++ {
    sum += i
  }
  return sum
}

func Sum2(start, end int64) int64 {
  if start > end {
    start, end = end, start
  }
  return (end - start + 1) * (end + start) / 2
}

func main() {

  sum1 := timedSumFunc(Sum1)
  sum2 := timedSumFunc(Sum2)

  fmt.Printf("%d, %d\n", sum1(-10000, 10000000), sum2(-10000, 10000000))
}

There are several things to explain about the above code:

1) There are two sum functions. Sum1() function simply makes a loop, and Sum2() function uses data formula. (Note: start and end may have negative numbers)

2) The code uses the reflection machine of Go language to obtain the function name.

3) The modifier function is timedSumFunc()

Output after operation:

$ go run time.sum.go
--- Time Elapsed (main.Sum1): 3.557469ms ---
--- Time Elapsed (main.Sum2): 291ns ---
49999954995000, 49999954995000

An example of HTTP related

Let's take another example of processing HTTP requests.

First look at a simple HTTP Server code.

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(hello))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

The modification mode is used in the above code. The withserverheader () function is a Decorator, which passes in an HTTP Handlerfunc, and then return an overwritten version. The above example is relatively simple. You can add a Response Header with WithServerHeader().

So, we can write many such functions. As shown below, some write HTTP response headers, some write authentication cookies, some check authentication cookies, and some log

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithAuthCookie()")
        cookie := &http.Cookie{Name: "Auth", Value: "Pass", Path: "/"}
        http.SetCookie(w, cookie)
        h(w, r)
    }
}

func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithBasicAuth()")
        cookie, err := r.Cookie("Auth")
        if err != nil || cookie.Value != "Pass" {
            w.WriteHeader(http.StatusForbidden)
            return
        }
        h(w, r)
    }
}

func WithDebugLog(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithDebugLog")
        r.ParseForm()
        log.Println(r.Form)
        log.Println("path", r.URL.Path)
        log.Println("scheme", r.URL.Scheme)
        log.Println(r.Form["url_long"])
        for k, v := range r.Form {
            log.Println("key:", k)
            log.Println("val:", strings.Join(v, ""))
        }
        h(w, r)
    }
}
func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
    http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
    http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

Pipeline of multiple modifiers
In use, you need to cover the functions layer by layer, which doesn't seem very good. If you need more decorator s, the code will be ugly. Well, we can refactor it.

During refactoring, we need to write a tool function to traverse and call each decorator:

type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc

func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}

Then, we can use it like this.

http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))

Is this code easier to read? The function of pipeline comes out.

Generic modifier

However, there is still a small problem with Go's modifier mode - it seems that it can't be generic. Like the time calculation function above, its code is coupled with the interface type of the function to be modified, so it can't be very general. If this problem can't be solved, this modifier mode is still a little difficult to use.

Because Go language is not like Python and Java, Python is a dynamic language, and Java has a language virtual machine, so they can do some abnormal things. However, Go language is a static language, which means that its type needs to be done at compile time, otherwise it cannot be compiled. However, the biggest generic type supported by Go language is interface {} and a relatively simple reflection mechanism. It should still be possible to work on it.

Needless to say, here is a general modifier I wrote with the reflection mechanism (for ease of reading, I deleted the error judgment code)

func Decorator(decoPtr, fn interface{}) (err error) {
    var decoratedFunc, targetFunc reflect.Value

    decoratedFunc = reflect.ValueOf(decoPtr).Elem()
    targetFunc = reflect.ValueOf(fn)

    v := reflect.MakeFunc(targetFunc.Type(),
            func(in []reflect.Value) (out []reflect.Value) {
                fmt.Println("before")
                out = targetFunc.Call(in)
                fmt.Println("after")
                return
            })

    decoratedFunc.Set(v)
    return
}

The above code uses reflect The makefunc() function makes a new function, in which targetfunc Call (in) calls the modified function. About the reflection mechanism of Go language, I recommend the official article - "The Laws of Reflection", which I won't say more here.

The Decorator() above requires two parameters,

The first is the parameter decoPtr, which is the modified function
The second is the input parameter fn, which is the function to be modified
Is there something wrong with writing like this? Indeed. However, this is the best code I can write in Go language. If you know more elegant, please tell me!

OK, let's see the effect. First, suppose we have two functions that need to be modified:

func foo(a, b, c int) int {
    fmt.Printf("%d, %d, %d \n", a, b, c)
    return a + b + c
}

func bar(a, b string) string {
    fmt.Printf("%s, %s \n", a, b)
    return a + b
}

Then we can do this:

type MyFoo func(int, int, int) int
var myfoo MyFoo
Decorator(&myfoo, foo)
myfoo(1, 2, 3)

You will find that when using Decorator(), you also need to declare a function signature first. It feels so silly. It's not generic at all, is it?

Um. If you don't want to declare a function signature, you can do the same

mybar := bar
Decorator(&mybar, bar)
mybar("hello,", "world!")

Well, it doesn't look so beautiful, but it works. It seems that the current characteristics of Go language can not be like Java or Python. For this, we can only ask for more sugar from Go language!

Again, if you have a better way of writing, please be sure to tell me.

(end of the full text) this article is not made by myself. It reprints the left ear mouse blog and its source Cool shell – CoolShell

Topics: Go