Get Started with the Dagger Go SDK
Introduction​
This tutorial teaches you the basics of using Dagger in Go. You will learn how to:
- Install the Go SDK
- Create a Go CI tool that builds a Go application for multiple architectures and Go versions using the Go SDK
Requirements​
This tutorial assumes that:
- You have a basic understanding of the Go programming language. If not, read the Go tutorial.
- You have a Go development environment with Go 1.15 or later. If not, download and install Go.
- You have Docker installed and running on the host system. If not, install Docker.
This tutorial creates a Go CI tool using the Dagger Go SDK. It uses this tool to build the Go application in the current directory. The binary from this guide can be used to build any Go project.
Step 1: Create a Go module for the tool​
The first step is to create a new Go module for the tool.
mkdir multibuild
cd multibuild
go mod init multibuild
Step 2: Create a Dagger client in Go​
If you would prefer to use the final main.go
file right away, it can be found in Step 5
Create a new file named main.go
and add the following code to it.
package main
import (
"context"
"fmt"
"dagger.io/dagger"
)
func main() {
if err := build(context.Background()); err != nil {
fmt.Println(err)
}
}
func build(ctx context.Context) error {
fmt.Println("Building with Dagger")
// initialize Dagger client
client, err := dagger.Connect(ctx)
if err != nil {
return err
}
defer client.Close()
return nil
}
This Go CI tool stub imports the Dagger SDK and defines two functions: main()
, which provides an interface for the user to pass in an argument to the tool and build()
, which defines the pipeline operations.
The build()
function creates a Dagger client with dagger.Connect()
. This client provides an interface for executing commands against the Dagger engine. This function is sparse to begin with; it will be improved in subsequent steps.
Step 3: Add the Dagger Go SDK to the module​
The Dagger Go SDK requires Go 1.15 or later.
From your existing Go module, install the Dagger Go SDK using the commands below:
go get dagger.io/dagger@latest
After importing dagger.io/dagger
in your Go module code, run the following command to update go.sum
:
go mod tidy
Try the Go CI tool by executing the commands below:
go run main.go
The tool outputs the string below, although it isn't actually building anything yet.
Building with Dagger
Step 4: Create a single-build pipeline​
Now that the basic structure of the Go CI tool is defined and functional, the next step is to flesh out its build()
function to actually build the Go application.
Replace the main.go
file from the previous step with the version below (highlighted lines indicate changes):
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
if err := build(context.Background()); err != nil {
fmt.Println(err)
}
}
func build(ctx context.Context) error {
fmt.Println("Building with Dagger")
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
return err
}
defer client.Close()
// get reference to the local project
src := client.Host().Directory(".")
// get `golang` image
golang := client.Container().From("golang:latest")
// mount cloned repository into `golang` image
golang = golang.WithMountedDirectory("/src", src).WithWorkdir("/src")
// define the application build command
path := "build/"
golang = golang.WithExec([]string{"go", "build", "-o", path})
// get reference to build output directory in container
output := golang.Directory(path)
// write contents of container build/ directory to the host
_, err = output.Export(ctx, path)
if err != nil {
return err
}
return nil
}
The revised build()
function is the main workhorse here, so let's step through it in detail.
- It begins by creating a Dagger client with
dagger.Connect()
, as before. - It uses the client's
Host().Directory()
method to obtain a reference to the current directory on the host. This reference is stored in thesrc
variable. - It initializes a new container from a base image with the
Container().From()
method and returns a newContainer
struct. In this case, the base image is thegolang:latest
image. - It mounts the filesystem of the repository branch in the container using the
WithMountedDirectory()
method of theContainer
.- The first argument is the target path in the container (here,
/src
). - The second argument is the directory to be mounted (here, the reference previously created in the
src
variable). It also changes the current working directory to the/src
path of the container using theWithWorkdir()
method and returns a revisedContainer
with the results of these operations.
- The first argument is the target path in the container (here,
- It uses the
WithExec()
method to define the command to be executed in the container - in this case, the commandgo build -o PATH
, wherePATH
refers to thebuild/
directory in the container. TheWithExec()
method returns a revisedContainer
containing the results of command execution. - It obtains a reference to the
build/
directory in the container with theDirectory()
method. - It writes the
build/
directory from the container to the host using theDirectory.Export()
method.
Try the tool by executing the commands below:
go run main.go
The Go CI tool builds the current Go project and writes the build result to build/
on the host.
Use the tree
command to see the build artifact on the host, as shown below:
tree build
build
└── multibuild
Step 5: Create a multi-build pipeline​
Now that the Go CI tool can build a Go application and output the build result, the next step is to extend it for multiple OS and architecture combinations.
Replace the main.go
file from the previous step with the version below (highlighted lines indicate changes):
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
if err := build(context.Background()); err != nil {
fmt.Println(err)
}
}
func build(ctx context.Context) error {
fmt.Println("Building with Dagger")
// define build matrix
oses := []string{"linux", "darwin"}
arches := []string{"amd64", "arm64"}
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
return err
}
defer client.Close()
// get reference to the local project
src := client.Host().Directory(".")
// create empty directory to put build outputs
outputs := client.Directory()
// get `golang` image
golang := client.Container().From("golang:latest")
// mount cloned repository into `golang` image
golang = golang.WithMountedDirectory("/src", src).WithWorkdir("/src")
for _, goos := range oses {
for _, goarch := range arches {
// create a directory for each os and arch
path := fmt.Sprintf("build/%s/%s/", goos, goarch)
// set GOARCH and GOOS in the build environment
build := golang.WithEnvVariable("GOOS", goos)
build = build.WithEnvVariable("GOARCH", goarch)
// build application
build = build.WithExec([]string{"go", "build", "-o", path})
// get reference to build output directory in container
outputs = outputs.WithDirectory(path, build.Directory(path))
}
}
// write build artifacts to host
_, err = outputs.Export(ctx, ".")
if err != nil {
return err
}
return nil
}
This revision of the Go CI tool does much the same as before, except that it now supports building the application for multiple OSs and architectures.
- It defines the build matrix, consisting of two OSs (
darwin
andlinux
) and two architectures (amd64
andarm64
). - It iterates over this matrix, building the Go application for each combination. The Go build process is instructed via the
GOOS
andGOARCH
build variables, which are reset for each case via theContainer.WithEnvVariable()
method. - It creates an output directory on the host named for each OS/architecture combination so that the build outputs can be differentiated.
Try the Go CI tool by executing the commands below:
go run main.go
The Go CI tool builds the application for each OS/architecture combination and writes the build results to the host. You will see the build process run four times, once for each combination. Note that the each build is happening concurrently, because each build in the DAG do not depend on eachother.
Use the tree
command to see the build artifacts on the host, as shown below:
tree build
build/
├── darwin
│  ├── amd64
│  │  └── multibuild
│  └── arm64
│  └── multibuild
└── linux
├── amd64
│  └── multibuild
└── arm64
└── multibuild
Another common operation in a CI environment involves creating builds targeting multiple Go versions. To do this, extend the Go CI tool further and replace the main.go
file from the previous step with the version below (highlighted lines indicate changes):
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
if err := build(context.Background()); err != nil {
fmt.Println(err)
}
}
func build(ctx context.Context) error {
fmt.Println("Building with Dagger")
// define build matrix
oses := []string{"linux", "darwin"}
arches := []string{"amd64", "arm64"}
goVersions := []string{"1.18", "1.19"}
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
return err
}
defer client.Close()
// get reference to the local project
src := client.Host().Directory(".")
// create empty directory to put build outputs
outputs := client.Directory()
for _, version := range goVersions {
// get `golang` image for specified Go version
imageTag := fmt.Sprintf("golang:%s", version)
golang := client.Container().From(imageTag)
// mount cloned repository into `golang` image
golang = golang.WithMountedDirectory("/src", src).WithWorkdir("/src")
for _, goos := range oses {
for _, goarch := range arches {
// create a directory for each os, arch and version
path := fmt.Sprintf("build/%s/%s/%s/", version, goos, goarch)
// set GOARCH and GOOS in the build environment
build := golang.WithEnvVariable("GOOS", goos)
build = build.WithEnvVariable("GOARCH", goarch)
// build application
build = build.WithExec([]string{"go", "build", "-o", path})
// get reference to build output directory in container
outputs = outputs.WithDirectory(path, build.Directory(path))
}
}
}
// write build artifacts to host
_, err = outputs.Export(ctx, ".")
if err != nil {
return err
}
return nil
}
This revision of the Go CI tool adds another layer to the build matrix, this time for Go language versions. Here, the build()
function uses the Go version number to download the appropriate Go base image for each build. It also adds the Go version number to each build output directory on the host to differentiate the build outputs.
Try the Go CI tool by executing the commands below:
go run main.go
The Go CI tool builds the application for each OS/architecture/version combination and writes the results to the host. You will see the build process run eight times, once for each combination. Note that the builds are happening concurrently, because each build in the DAG does not depend on any other build.
Use the tree
command to see the build artifacts on the host, as shown below:
tree build
build/
├── 1.18
│  ├── darwin
│  │  ├── amd64
│  │  │  └── multibuild
│  │  └── arm64
│  │  └── multibuild
│  └── linux
│  ├── amd64
│  │  └── multibuild
│  └── arm64
│  └── multibuild
└── 1.19
├── darwin
│  ├── amd64
│  │  └── multibuild
│  └── arm64
│  └── multibuild
└── linux
├── amd64
│  └── multibuild
└── arm64
└── multibuild
As the previous steps illustrate, the Dagger Go SDK allows you to author your pipeline entirely in Go. This means that you don't need to spend time learning a new language, and you immediately benefit from all the powerful programming capabilities and packages available in Go. For instance, this tutorial used native Go variables, conditionals and error handling throughout, together with the errgroup package for sub-task parallelization.
Conclusion​
This tutorial introduced you to the Dagger Go SDK. It explained how to install the SDK and use it with a Go module. It also provided a working example of a Go CI tool powered by the SDK, which is able to build an application for multiple OSs, architectures and Go versions in parallel.
Use the SDK Reference to learn more about the Dagger Go SDK.