Categories
Go Intermediate tutorial

Goroutines and the ease of use

What is the most efficient way to use goroutines in simple terms for scalable service?

Goroutines are a powerful feature of the Go programming language that allows you to achieve concurrency and scalability in your applications. Here’s a simple explanation of how to use goroutines efficiently for a scalable service:

  1. Understand Concurrency: Goroutines are lightweight threads that enable concurrent execution. Concurrency is about making progress on multiple tasks simultaneously. It’s important to understand the concept of concurrency and how it differs from parallelism.
  2. Identify Tasks: Identify the tasks that can be executed independently or in parallel. These tasks could be parts of your service that can run concurrently, such as handling incoming requests, processing data, or performing I/O operations.
  3. Use Goroutines: Launch goroutines to execute the identified tasks concurrently. Goroutines are created using the go keyword followed by a function call. For example: go processRequest(request). Each goroutine represents a separate flow of execution.
  4. Communicate with Channels: To synchronize and communicate between goroutines, use channels. Channels allow goroutines to send and receive values safely. You can create a channel using the make function and send or receive data using the <- operator.
  5. Utilize WaitGroups: To ensure that all goroutines finish their work before the program exits, you can use the sync.WaitGroup type. It provides a mechanism to wait for a collection of goroutines to complete. Increment the wait group counter before launching a goroutine, and decrement it when the goroutine completes its work.
  6. Avoid Unbounded Goroutines: Be cautious when creating goroutines, as creating too many goroutines can lead to excessive resource consumption and hinder performance. You can limit the number of concurrent goroutines by using techniques such as worker pools or semaphores.
  7. Handle Errors: Ensure you handle errors appropriately in your goroutines. If an error occurs in a goroutine, handle it locally or communicate it back to the main goroutine through a channel or other means.
  8. Monitor Resource Usage: Keep an eye on the resource usage of your service, including CPU, memory, and I/O. This will help you optimize the number of goroutines and identify any performance bottlenecks.

Remember that efficient use of goroutines involves a good understanding of your application’s requirements and characteristics. It’s essential to balance concurrency and resource usage to achieve the desired scalability.

What is an example of a simple Goroutine use?

package main

import (
	"fmt"
	"time"
)

func main() {
	// Launch a goroutine to perform a time-consuming task
	go performTask()

	// Continue with other tasks in the main goroutine
	for i := 1; i <= 5; i++ {
		fmt.Println("Main Goroutine: Doing some other work")
		time.Sleep(1 * time.Second)
	}

	// Wait for a few seconds to allow the goroutine to complete its task
	time.Sleep(3 * time.Second)
}

func performTask() {
	fmt.Println("Goroutine: Starting time-consuming task")
	time.Sleep(2 * time.Second)
	fmt.Println("Goroutine: Time-consuming task completed")
}

In this example, we launch a goroutine using the go keyword followed by the function call to performTask(). The performTask() function simulates a time-consuming task by sleeping for 2 seconds.

While the goroutine is executing the time-consuming task, the main goroutine continues to execute its own tasks in the loop, printing “Main Goroutine: Doing some other work” every second.

After waiting for a few seconds to allow the goroutine to complete its task, the program exits. You will see output from both the main goroutine and the goroutine executing the time-consuming task interleaved in the console.

By using goroutines, you can perform tasks concurrently and effectively utilize the available resources.

A complex example of how to use a Goroutine with Channels

package main

import (
	"crypto/md5"
	"fmt"
	"io"
	"net/http"
	"os"
)

type File struct {
	URL      string
	FileName string
	Checksum string
}

func main() {
	fileURLs := []string{
		"https://example.com/file1.txt",
		"https://example.com/file2.txt",
		"https://example.com/file3.txt",
	}

	fileChannel := make(chan File)
	doneChannel := make(chan bool)

	// Launch goroutines to download files concurrently
	for _, url := range fileURLs {
		go downloadFile(url, fileChannel)
	}

	// Wait for all downloads to complete
	go func() {
		for range fileURLs {
			<-doneChannel
		}
		close(fileChannel)
	}()

	// Calculate checksums for downloaded files
	for file := range fileChannel {
		go calculateChecksum(&file, doneChannel)
	}

	// Wait for all checksum calculations to complete
	for range fileURLs {
		<-doneChannel
	}

	fmt.Println("All files downloaded and checksums calculated successfully.")
}

func downloadFile(url string, fileChannel chan<- File) {
	response, err := http.Get(url)
	if err != nil {
		fmt.Printf("Error downloading %s: %s\n", url, err.Error())
		return
	}
	defer response.Body.Close()

	fileName := getFileNameFromURL(url)
	output, err := os.Create(fileName)
	if err != nil {
		fmt.Printf("Error creating file %s: %s\n", fileName, err.Error())
		return
	}
	defer output.Close()

	_, err = io.Copy(output, response.Body)
	if err != nil {
		fmt.Printf("Error downloading %s: %s\n", url, err.Error())
		return
	}

	fileChannel <- File{URL: url, FileName: fileName}
}

func calculateChecksum(file *File, doneChannel chan<- bool) {
	checksum, err := calculateMD5Checksum(file.FileName)
	if err != nil {
		fmt.Printf("Error calculating checksum for %s: %s\n", file.FileName, err.Error())
		return
	}

	file.Checksum = checksum
	fmt.Printf("Checksum calculated for %s: %s\n", file.FileName, checksum)

	doneChannel <- true
}

func calculateMD5Checksum(fileName string) (string, error) {
	file, err := os.Open(fileName)
	if err != nil {
		return "", err
	}
	defer file.Close()

	hash := md5.New()
	if _, err := io.Copy(hash, file); err != nil {
		return "", err
	}

	checksum := hash.Sum(nil)
	return fmt.Sprintf("%x", checksum), nil
}

func getFileNameFromURL(url string) string {
	// Extract the file name from the URL
	// You can implement your own logic based on the URL structure
	return "downloaded_file.txt"
}

In this example, we have a list of file URLs that we want to download concurrently. We create two channels: fileChannel to communicate downloaded files to the checksum calculation goroutines and doneChannel to signal completion of individual goroutines.

We launch a goroutine for each file URL in the downloadFile function, which downloads the file and sends the file information through the fileChannel. After all the downloads are completed, we close the fileChannel to signal the checksum calculation goroutines to exit.

In the calculateChecksum function, we receive the file information from the fileChannel, calculate its checksum, and update the Checksum field of

Categories
AWS Beginner DynamoDB Go Intermediate tutorial

Testing locally DynamoDB in Go

Testing locally DynamoDB in Go
Testing locally DynamoDB in Go

Testing DynamoDB locally in Go or any language is tricky. In this quick article I will provide a solution on how to test locally using a docker container and your web service that interfaces with AWS DynamoDB.

First, you will need a DynamoDB instance running locally so that you can interact with it from your service.

DynamoDB Instance with Docker-Compose

dynamodb:
    image:  "amazon/dynamodb-local:latest"
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    container_name: dynamodb
    ports:
      - 8000:8000
    environment:
      AWS_REGION: "us-east-1"
      AWS_ACCESS_KEY_ID: "YOUR_AKID"
      AWS_SECRET_ACCESS_KEY: "YOUR_SECRET_KEY"
      AWS_SESSION_TOKEN: "TOKEN"
    working_dir: "/home/dynamodblocal"
    volumes:
      -  "./docker/dynamodb:/home/dynamodblocal/data"

The code above shows you how to create a simple DynamoDB instance for you to test with locally user a docker-compose.yml file. In this article we are assuming that you are running your webservice that has visibility to the DynamoDB container, the setup of the web service is out of scope for this article.

When using AWS SDK v2 for Golang things are done way differently then on version v1. In v1, you could provide an endpoint, a region and credentials that required for you to test locally. Just for reference, this is how you test in aws-sdk-go-v1:

        creds := credentials.NewEnvCredentials()
	_, err := creds.Get()
	if err != nil {
		return nil, err
	}
	sess, err := session.NewSession(&aws.Config{
		Region:      aws.String("us-east-1"),
		Endpoint:    aws.String("http://localhost:8000"),
		Credentials: creds},
	)
	if err != nil {
		return nil, err
	}
	// Create DynamoDB client
	svc := dynamodb.New(sess)

The endpoint value is whatever url you specified in your docker-compose file for you AWS DynamoDB instance.

Implement AWS SDK Go v2

Now to answer the question of this topic: Testing locally DynamoDB, how can we accomplish this? Compared to v1, we have to do things differently. In v2 you have to have a several awsconfig.LoadOptionFunc’s. You will need the WithRegion, WithEndpointResolver, and lastly for local testing WithCredentialsProvider. For v2, make sure you have an IAM Role that is able to talk to your ec2 instance or whatever infrastructure you are using to serve your app and the DynamoDB instance.

customResolver := aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
	if os.Getenv("APP_ENV") == "local" {
		return aws.Endpoint{
			URL: config.Dynamo.Endpoint,
		}, nil
	}
	// returning EndpointNotFoundError will allow the service to fallback to it's default resolution
	return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})
var (
	cfg aws.Config
	err error
)
if os.Getenv("APP_ENV") == "local" {
	cfg, err = awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion("us-east-1"), awsconfig.WithEndpointResolver(customResolver), awsconfig.WithCredentialsProvider(credentials.StaticCredentialsProvider{
		Value: aws.Credentials{
			AccessKeyID: "dummy", SecretAccessKey: "dummy", SessionToken: "dummy",
		Source: "Hard-coded credentials; values are irrelevant for local DynamoDB",
		},
	}))
} else {
	cfg, err = awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion("us-east-1"), awsconfig.WithEndpointResolver(customResolver))
}	
if err != nil {
	panic(err)
}
svc := dynamodb.NewFromConfig(cfg)

In the code above, you will notice that I check for an environment variable to equal “local”. The reason for this is, in v2 you are discouraged from using AWS access keys. When you test locally, the container requires an AWS access key, therefore you can create dummy credentials to use when testing locally.

Thank you for taking the time to read this information. You can find other articles here.