Go Functions

Writing Go functions with fission

Write, build, and deploy Go functions on Fission using its native Go plugin mechanism.

This guide covers setting up the Go environment, writing functions, handling HTTP requests/responses, and managing dependencies.

Before you start

We’ll assume you have Fission and Kubernetes setup. If not, head over to the install guide. Verify your Fission setup with:

fission version

Add the Go environment to your cluster

Unlike Python, Go is a compiled language, so source code must be compiled before it can run. Fission’s builder manager handles this automatically when a Go function/package is created, converting a source package into a deployable package.

Due to the Go plugin mechanism, the Go plugin can only be loaded by the server with the exact same Go version. Please use the fission release version as image tag instead of latest when adding a Go environment, so that you won’t experience the compatibility issue once we bump up the Go version. (Remember, the image tag of runtime and builder should always be the same.)

$ fission environment create --name go --image ghcr.io/fission/go-env:<release-version> --builder ghcr.io/fission/go-builder:<release-version> --version 3

# Example
$ fission environment create --name go --image ghcr.io/fission/go-env-1.26 --builder ghcr.io/fission/go-builder-1.26

You can find all images and image tags at following table. To get an overall overview of all available images, please have a look at our Docker Hub page.

Go environment image list

Go VersionImageBuilder Image
1.26ghcr.io/fission/go-env-1.26ghcr.io/fission/go-builder-1.26

Write a simple function in Go

Here is a hello world example (hw.go) in Go:

// Due to go plugin mechanism,
// the package of function handler must be main package
package main

import (
    "net/http"
)

// Handler is the entry point for this fission function
func Handler(w http.ResponseWriter, r *http.Request) {
    msg := "Hello, world!\n"
    w.Write([]byte(msg))
}

The entrypoint tells the Go server how to load the plugin file correctly. Here, our first hello world function’s entrypoint is the name of the function: Handler.

# Create golang env with builder image to build go plugin
$ fission fn create --name helloworld --env go --src hw.go --entrypoint Handler

Before accessing the function, ensure the deployed package is in the succeeded state.

fission pkg info --name <pkg-name>
For archive of package with size larger than 256 KB, the package is uploaded to storage service. Double check fission-storage-pvc is properly setup if pkg BUILD_STATUS is not succeeded.

Now, let’s test our first Go function with test command

fission fn test --name <function-name>
See here for how to setup different trigger for Go function.

HTTP requests and HTTP responses

The sample above shows the Go server passing the HTTP Request and ResponseWriter to the function for further processing.

func Handler(w http.ResponseWriter, r *http.Request) {
    ...
}

The following sections show how to access requests and control responses.

Accessing HTTP Requests

Headers
package main

import (
    "fmt"
    "net/http"
    "net/textproto"
)

func Handler(w http.ResponseWriter, r *http.Request) {

    // recommend
    v1 := r.Header.Get("HEADER_KEY_1")

    // need to convert string cases manually
    key := textproto.CanonicalMIMEHeaderKey("HEADER_KEY_2")
    // v2 type is []string
    v2, _ := r.Header[key]

    w.Write([]byte(fmt.Sprintf("v1: %s, v2: %v", v1, v2)))
}

Let’s create a HTTP trigger for the function we created above.

fission httptrigger create --method GET --url "/<url>" --function <fn-namne>
$ curl -X GET http://$FISSION_ROUTER/<url> -H 'HEADER_KEY_1: foo' -H 'HEADER_KEY_2: bar'
v1: foo, v2: [bar]

All header keys are converted to the canonical format of the MIME header key. You can access the header value by calling request.Header.Get(). However, if you prefer to access the value by map index you must convert key format with textproto.CanonicalMIMEHeaderKey().

Query string
package main

import (
    "net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
    v := r.URL.Query().Get("key-name")
    w.Write([]byte(v))
}
$ curl -X GET http://$FISSION_ROUTER/<url>?key-name=123
123
Request Body
PLAIN TEXT
package main

import (
    "io/ioutil"
    "net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
    reqBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte(err.Error()))
    }

    body := string(reqBody)

    w.Write([]byte(body))
}
$ curl -X POST http://$FISSION_ROUTER/<url> -d foobar
foobar
JSON
package main

import (
    "encoding/json"
    "net/http"
    "io/ioutil"
)

type msg struct {
    Content string `json:"content"`
}

func Handler(w http.ResponseWriter, r *http.Request) {
    reqBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte(err.Error()))
    }

    m := msg{}
    err = json.Unmarshal(reqBody, &m)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte(err.Error()))
    }

    respBody, err := json.Marshal(m)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
    }

    w.Write([]byte(respBody))
}
curl -X POST http://$FISSION_ROUTER/<url> -d '{"content": "foobar"}'
{"content":"foobar"}

Controlling HTTP Responses

Setting Response Headers
package main

import (
    "net/http"
)

func setCORS(w http.ResponseWriter) http.ResponseWriter {
    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Allow-Headers", "*")
    w.Header().Set("Access-Control-Allow-Credentials", "true")
    w.Header().Set("Access-Control-Allow-Methods", "*")
    w.Header().Set("Access-Control-Expose-Headers", "*")
    return w
}

func Handler(w http.ResponseWriter, r *http.Request) {
    w = setCORS(w)
    w.WriteHeader(http.StatusOK)
}
$ curl -i -X GET "http://$FISSION_ROUTER/<url>"
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: *
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Date: Sat, 27 Oct 2018 15:00:14 GMT
Setting Status Codes
Always write response status code before writing response body! Otherwise, the client may receive unexpected status code.

You can set the response status code by calling writer.WriteHeader(). If a function writes the response body before the status code, the client receives HTTP 200 OK regardless of the actual status code.

package main

import (
    "net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
    // DONT
    // w.Write([]byte("foobar"))
    // w.WriteHeader(http.StatusBadRequest)

    // DO
    http.Error(w, "dummy error", http.StatusBadRequest)

    // or DO
    // w.WriteHeader(http.StatusBadRequest)
    // w.Write([]byte("foobar"))
}

Working with dependencies

See full image list here

Go module support

Please use image version equal or after ghcr.io/fission/go-env-1.26.

Initialize your project

$ go mod init "<module>"

# Example
$ go mod init "github.com/fission/fission/examples/go/go-module-example"

Follow the official guide to add project dependencies. To clean and verify the dependencies you can:

go mod tidy
go mod verify

Archive and create package as usual

$ zip -r go.zip .
    adding: go.mod (deflated 26%)
    adding: go.sum (deflated 1%)
    adding: README.md (deflated 37%)
    adding: main.go (deflated 30%)

$ fission pkg create --env go --src go.zip

Add dependencies to vendor directory

This part is for the old Go environment that doesn't support Go Module

Unlike pip for Python has been widely adopted by community, there are various dependency management tools for Go like gb, dep and glide. Hence fission Go builder image doesn’t contain a default tool for downloading dependencies during build processes.

To support 3rd party dependencies, users need to put all necessary packages into the vendor directory and archive it into the source archive.

The example below uses glide to demonstrate how to add dependencies to a source archive.

$ mkdir example
$ pushd example
$ glide init --non-interactive
$ glide get "github.com/golang/example/stringutil"
$ curl https://raw.githubusercontent.com/fission/examples/main/go/vendor-example/main.go > main.go
$ zip -r example.zip .
$ fission fn create --name foobar --src example.zip --entrypoint Handler
# wait for package build process
$ fission fn test --name foobar

Custom builds

If no custom build script included in source archive, by default builder manager will execute the build.sh in go builder image. In this part, we will go through how to run custom build script during the build process.

Before we write our own script, we need to dive into the original build script to see what’s useful inside.

#!/bin/sh

set -eux

srcDir=${GOPATH}/src/$(basename ${SRC_PKG})

trap "rm -rf ${srcDir}" EXIT

if [ -d ${SRC_PKG} ]
then
    echo "Building in directory ${srcDir}"
    ln -sf ${SRC_PKG} ${srcDir}
elif [ -f ${SRC_PKG} ]
then
    echo "Building file ${SRC_PKG} in ${srcDir}"
    mkdir -p ${srcDir}
    cp ${SRC_PKG} ${srcDir}/function.go
fi

cd ${srcDir}
go build -buildmode=plugin -o ${DEPLOY_PKG} .

There are environment variables as shown above:

  1. $SRC_PKG: The path to the source code directory/file.
  2. $DEPLOY_PKG: The path to the deploy directory/executable binary.

The steps of build script are:

  1. Check the $SRC_PKG is a file or directory
  2. Copy/Link source code to $GOPATH
  3. Change location to $SRC_PKG, build source code into plugin file then output to $DEPLOY_PKG

For example, we want to add git commit SHA or build timestamp to go binaries like following.

package main

import (
    "fmt"
    "net/http"
)

var (
    Timestamp string
)

// Handler is the entry point for this fission function
func Handler(w http.ResponseWriter, r *http.Request) {
    msg := fmt.Sprintf("Build timestamp: %v", Timestamp)
    w.Write([]byte(msg))
}

The build timestamp can be set through ldflags during the build process. The original build script doesn’t support this, so it needs to be modified.

Since we understand how a build script (customBuild.sh) works, let’s try to modify it a little bit.

#!/bin/sh

set -eux

srcDir=${GOPATH}/src/$(basename ${SRC_PKG})

trap "rm -rf ${srcDir}" EXIT

if [ -d ${SRC_PKG} ]
then
    echo "Building in directory ${srcDir}"
    ln -sf ${SRC_PKG} ${srcDir}
elif [ -f ${SRC_PKG} ]
then
    echo "Building file ${SRC_PKG} in ${srcDir}"
    mkdir -p ${srcDir}
    cp ${SRC_PKG} ${srcDir}/function.go
fi


pkgpath=$(basename ${SRC_PKG})
timestamp=$(date +%s)

cd ${srcDir}
go build -ldflags "-X ${pkgpath}.Timestamp=$timestamp" -buildmode=plugin -o ${DEPLOY_PKG} .

At this moment, the directory structure would be like:

.
├── customBuild.sh
└── main.go

Archive source package.

chmod +x customBuild.sh
zip -r example.zip .

After editing custom build script, next step is to assign a build command for builder to know what command to run with.

$ fission fn create --name foobar --env go --src example.zip --entrypoint Handler --buildcmd "./customBuild.sh"
$ fission fn test --name foobar
Build timestamp: 1540566887

Modifying/Rebuild the environment images

Refer to our Go environment guide for Fission to learn about rebuilding environment images.

Resource usage

By default, a function can use all resource on node it runs on, however, other functions will be affected once a function takes too many resources. To prevent this, Fission allows users to specify a resource limitation for function.

Following are flags to manage function resource usage limitation.

--mincpu value                         Minimum CPU to be assigned to pod (In millicore, minimum 1)
--maxcpu value                         Maximum CPU to be assigned to pod (In millicore, minimum 1)
--minmemory value                      Minimum memory to be assigned to pod (In megabyte)
--maxmemory value                      Maximum memory to be assigned to pod (In megabyte)

And you can specify them when creating/updating a function.

$ fission fn create --name g1 --env go --src example.zip --entrypoint Handler \
                    --mincpu 20 --maxcpu 100 --minmemory 128 --maxmemory 256

The right resource setting for a function depends on what the function does.

Use kubectl top to get the actual resource consumption of the pod while benchmarking, so you know the best setting for a Go function.

$ kubectl top pod -l functionName=g1
NAME                                                              CPU(cores)   MEMORY(bytes)
go-1ef65ed0-d5eb-11e8-91e8-080027114863-9x7y4xmh-569bfdd9bhmlrf   112m           6Mi