Go Functions
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 Version | Image | Builder Image |
|---|---|---|
| 1.26 | ghcr.io/fission/go-env-1.26 | ghcr.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 checkfission-storage-pvcis properly setup if pkgBUILD_STATUSis notsucceeded.
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:
- $SRC_PKG: The path to the source code directory/file.
- $DEPLOY_PKG: The path to the deploy directory/executable binary.
The steps of build script are:
- Check the $SRC_PKG is a file or directory
- Copy/Link source code to $GOPATH
- 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