
Build a Temporal Application from scratch in Go
- Build the application
- Test and run a Worker
- Run and observe retries
In this tutorial, you'll build your first Temporal Application from scratch using the Temporal Go SDK. You'll develop a small application that asks for your name and then uses APIs to get your public IP address and your location based on that address. External requests can fail due to rate limiting, network interruptions, or other errors. Using Temporal for this application will let you automatically recover from these and other kinds of failures without having to write explicit error-handling code.
The app will consist of the following pieces:
- Two Activities: the first gets your IP address, and the second uses that IP to find your location.
- A Workflow that calls both Activities, using the result of the first as input to the second.
- A Worker to host the Workflow and Activity code.
- A client program to start your Workflow.
You'll also write tests to verify your Workflow runs successfully.
Prerequisites
Before starting this tutorial:
- Set up a local development environment for developing Temporal Applications with Go. Ensure the Temporal Service is running locally and you can access the Web UI on port
8233(the default). - Follow the Run your first Temporal application with the Go SDK tutorial to understand how Temporal's components fit together.
Create a new Go project
To get started with the Temporal Go SDK, you'll create a new Go project and initialize it as a module, just like any other Go program. Then you'll add the Temporal SDK package to your project.
In a terminal, create a new project directory called temporal-ip-geolocation:
mkdir temporal-ip-geolocation
Switch to the new directory:
cd temporal-ip-geolocation
From the root of your new project directory, initialize a new Go module. Make sure the module path matches that of the directory in which you are creating the module.
go mod init temporal-ip-geolocation/iplocate
Then add the Temporal Go SDK as a project dependency:
go get go.temporal.io/sdk
You'll see the following output, indicating that the SDK is now a project dependency:
go: downloading go.temporal.io/sdk v1.31.0
go: added go.temporal.io/sdk v1.31.0
With the project created, you'll create the application's core logic.
Write functions to call external services
Your application will make two HTTP requests. The first returns your current public IP, while the second uses that IP to provide city, state, and country information.
You'll use Temporal Activities to make these requests. Activities are where you execute non-deterministic code or perform operations that may fail, such as API requests or database calls.
If an Activity fails, Temporal can automatically retry it until it succeeds or reaches a specified retry limit. This ensures that transient issues, like network glitches or temporary service outages, don't result in data loss or incomplete processes.
Create the file activities.go in your project root:
touch activities.go
Open activities.go in your editor and add the following code that imports dependencies and defines a struct to hold the Activities:
package iplocate
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type HTTPGetter interface {
Get(url string) (*http.Response, error)
}
type IPActivities struct {
HTTPClient HTTPGetter
}
With the Go SDK, you can define Activities as regular Go functions. You can also create them as members of a struct, which is necessary to pass shared objects like database connections. In this tutorial you'll pass an HTTP client into the struct so you can stub out the HTTP client when you write tests for your Activities later.
Now add the following code to define a Temporal Activity that retrieves your IP address from icanhazip.com:
// GetIP fetches the public IP address.
func (i *IPActivities) GetIP(ctx context.Context) (string, error) {
resp, err := i.HTTPClient.Get("https://icanhazip.com")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
ip := strings.TrimSpace(string(body))
return ip, nil
}
With the Temporal Go SDK, both Activities and Workflows expect a Go context as their first argument. This is necessary to enable a number of other SDK features.
Like other Go functions, Activities should return an error as one of their arguments, so it can be checked after the Activity completes. This is because Go does not use a try/catch construct like other languages.
The response from icanhazip.com is plain-text, and it includes a newline, so you trim off the newline character before returning the result.
Notice there's no error-handling code in this function. When you build your Workflow, you'll use Temporal's Activity Retry policies to retry this code automatically if there's an error.
Now add the second Activity that accepts an IP address and retrieves location data. In activities.go, add the following code to define it:
// GetLocationInfo uses the IP address to fetch location information.
func (i *IPActivities) GetLocationInfo(ctx context.Context, ip string) (string, error) {
url := fmt.Sprintf("http://ip-api.com/json/%s", ip)
resp, err := i.HTTPClient.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var data struct {
City string `json:"city"`
RegionName string `json:"regionName"`
Country string `json:"country"`
}
err = json.Unmarshal(body, &data)
if err != nil {
return "", err
}
return fmt.Sprintf("%s, %s, %s", data.City, data.RegionName, data.Country), nil
}
This Activity follows the same pattern as the GetIP Activity. This time, the service returns JSON data rather than text, so you have to define a type to unmarshal the data.
While Activities can accept input arguments, it's a best practice to send a single argument rather than multiple arguments. If you have more than one argument, bundle them up in a serializable object. Later revisions that change the number of arguments sent to a Workflow or Activity can otherwise introduce versioning concerns. Review the Activity parameters section of the Temporal documentation for details.
You've created your two Activities. Now you'll coordinate them using a Temporal Workflow.
Control application logic with a Workflow
Workflows are where you configure and organize the execution of Activities. You define a Workflow by writing a Workflow Definition using one of the Temporal SDKs.
In the Temporal Go SDK, a Workflow Definition is an exported function with two additional requirements: it must accept workflow.Context as the first input parameter, and it must return error. Your Workflow function can optionally return another value, which you'll use to return the result of the Workflow Execution.
Temporal Workflows must be deterministic so that Temporal can replay your Workflow in the event of a crash. That's why you call Activities from your Workflow code. Activities don't have the same determinism constraints that Workflows have.
Create the file workflows.go in the root of your project:
touch workflows.go
Then add the following code to import the Activities and configure how the Workflow should handle failures with a Retry Policy.
package iplocate
import (
"fmt"
"time"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
)
With the imports in place, you can define the Workflow itself. Add the following code to define the GetAddressFromIP Workflow, which calls both Activities, using the value of the first as the input to the second:
// GetAddressFromIP is the Temporal Workflow that retrieves the IP address and location info.
func GetAddressFromIP(ctx workflow.Context, name string) (string, error) {
// Define the activity options, including the retry policy
ao := workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second, //amount of time that must elapse before the first retry occurs
MaximumInterval: time.Minute, //maximum interval between retries
BackoffCoefficient: 2, //how much the retry interval increases
// MaximumAttempts: 5, // Uncomment this if you want to limit attempts
},
}
ctx = workflow.WithActivityOptions(ctx, ao)
var ipActivities *IPActivities
var ip string
err := workflow.ExecuteActivity(ctx, ipActivities.GetIP).Get(ctx, &ip)
if err != nil {
return "", fmt.Errorf("Failed to get IP: %s", err)
}
var location string
err = workflow.ExecuteActivity(ctx, ipActivities.GetLocationInfo, ip).Get(ctx, &location)
if err != nil {
return "", fmt.Errorf("Failed to get location: %s", err)
}
return fmt.Sprintf("Hello, %s. Your IP is %s and your location is %s", name, ip, location), nil
}
The function accepts a workflow.Context and a string value that holds the name. It returns a string value and an error, which follows the conventions you'll find in other Go programs. Like with Activities, you can send multiple inputs into a Workflow, but it's a good practice to combine those into a struct.
In this example, you've specified that the Start-to-Close Timeout for your Activities will be one minute, meaning that your Activity has one minute to complete before it times out. Of all the Temporal timeout options, StartToCloseTimeout is the one you should always set.
You also set the Retry Policy for Activities. In this example, you're using the default Retry Policy values, so you don't need to specify them, but leaving them in gives you a clearer picture of what happens. Note that MaximumAttempts is commented out, which means there's no limit to the number of times Temporal will retry your Activities if they fail.
This code does check for and handle errors, but because you've configured unlimited retries, there won't be any exceptions caught. However, if you change the Retry Policy's maximum retries, or you specify non-retryable exceptions, this code will be in place to handle those errors.
Next you'll create a Worker that executes the Workflow and Activities, and write tests to confirm everything works as expected.
Get notified when we launch new educational content
New courses, tutorials, and learning resources - straight to your inbox.