Golang notes no. 4

May 22, 2023

Background Processing

When you're dealing with background processing in a separate goroutine, there are a couple of important things to keep in mind regarding context cancellation.

First, if a parent context gets canceled, this cancellation signal is passed down to its child contexts. So, be aware of this propagation when working with nested contexts.

Secondly, let's talk about the context cancellation that occurs with server requests. When you receive an incoming server request in Go, the request context is canceled automatically when the ServeHTTP method finishes executing.

Now, here's the interesting part: If you use a context that is derived from the request context in your background processing, the background process will also receive a cancellation signal when the HTTP response is sent for the initial request. This might not be what you want, and in most cases, you probably don't want it to happen.

To avoid this unexpected cancellation in your background process, it's best to create a fresh context specifically for the background task using context.Background(). This way, it won't be connected to the request context. If you have any values or data from the request context that you need in the background process, you can either copy them over to the new context or simply pass them as regular parameters to the background function.

By doing this, you ensure that the background process operates independently of the request context and won't be affected by any cancellations happening when the HTTP response is sent.

Remember, when working with background processing in Go, be mindful of context cancellation. Use a separate context for the background task, created with context.Background(), to avoid unexpected cancellations from the request context. Copy over necessary values or pass them as regular parameters. This way, you'll have more control over the cancellation behavior and ensure smooth execution of your background tasks.

Race Conditions

Imagine a race condition as a situation where two or more threads are trying to mess with the same shared data simultaneously. The tricky part is that the order in which these threads are scheduled can be unpredictable, like a race. So, depending on the timing, the outcome of changing the shared data can vary.

Here's an example to illustrate the problem: Let's say we have a variable x that is initially 5. One thread does a check on x to see if it's equal to 5. If it is, the thread multiplies x by 2 and assigns the result to another variable y. But here's the catch: if another thread modifies x in between the check and the multiplication, then y won't be equal to 10 as expected.

The issue lies in the uncertainty of when exactly each thread gets to execute its instructions. Depending on the thread scheduler, different threads may have a chance to modify x at any point during the "check-then-act" process, leading to inconsistent results.

To prevent such race conditions, we can use locks. By placing a lock around the shared data, we ensure that only one thread can access and modify it at any given time. In our example, before performing the check on x, we would acquire a lock specifically for x. This guarantees that no other thread can change x until the lock is released. So, even if another thread tries to interfere, it will have to wait until the lock is released before it can modify x.

By using locks, we can make sure that shared data is accessed and modified in a controlled and orderly manner, eliminating the uncertainties caused by race conditions.

import (
	"strconv"
	"sync"
)

var myBalance = &balance{amount: 50.00, currency: "GBP"}

type balance struct {
	amount	 float64
	currency string
	mu			 sync.Mutex
}

func (b *balance) Add(i float64) {
	b.mu.Lock()
	b.amount += i
	b.mu.Unlock()
}

func (b *balance) Display() string {
	b.mu.Lock()
	defer b.mu.Unlock()
	return strconv.FormatFloat(b.amount, 'f', 2, 64) + " " + b.currency
}

sync.RWMutex, a reader/writer mutual exclusion lock which allows any number of readers to hold the lock or one writer. This tends to be more efficient than using a full mutex in situations where you have a high ratio of reads to writes.

Reader locks can be opened and closed with the RLock() and RUnlock() methods like so:

import (
	"strconv"
	"sync"
)

var myBalance = &balance{amount: 50.00, currency: "GBP"}

type balance struct {
	amount	 float64
	currency string
	mu			 sync.RWMutex
}

func (b *balance) Add(i float64) {
	b.mu.Lock()
	b.amount += i
	b.mu.Unlock()
}

func (b *balance) Display() string {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return strconv.FormatFloat(b.amount, 'f', 2, 64) + " " + b.currency
}

In this scenario, we have a shared data structure called balance that represents a bank account balance. The balance struct contains an amount field and a currency field. To ensure that the shared data is accessed safely, we use synchronization techniques from the sync package in Go.

In the first version of the code, we use a sync.Mutex (mutual exclusion lock) to protect the balance data. The Add method acquires the lock with b.mu.Lock() before updating the balance amount and releases the lock with b.mu.Unlock() afterward. Similarly, the Display method acquires the lock to read the balance and then releases it using defer to ensure the lock is always released.

However, in situations where there are a lot of concurrent reads and only occasional writes, using a full mutex for synchronization might be inefficient. That's where the sync.RWMutex comes into play. It allows multiple readers to hold the lock simultaneously or a single writer to acquire an exclusive lock.

In the updated version of the code, we replace sync.Mutex with sync.RWMutex. The Add method remains the same since it performs a write operation, but the Display method is modified. Instead of using b.mu.Lock() and b.mu.Unlock(), it uses b.mu.RLock() to acquire a reader lock and b.mu.RUnlock() to release it. This allows multiple goroutines to read the balance concurrently without blocking each other. Only when a write operation is performed (like in the Add method) is an exclusive lock acquired.

By utilizing a reader/writer lock (sync.RWMutex), we can improve the efficiency of concurrent read operations while ensuring exclusive access for write operations. This helps to limit the possibility of race conditions and provides a more scalable solution for scenarios where there are a higher number of reads compared to writes.

Rate Limiting

Rate limiting is a technique used to control the rate of incoming requests to a system, preventing it from being overwhelmed and ensuring fair resource allocation. It's like having a bouncer at a club who allows only a certain number of people in at a time, maintaining order and preventing overcrowding.

In the context of Go, rate limiting is particularly useful for applications that provide APIs or services where clients make requests. By implementing rate limiting, you can protect your application from being flooded with requests and potentially becoming unresponsive or degraded.

Let's imagine a scenario where you have an API that allows clients to access certain resources or perform specific actions. Without rate limiting, a malicious or poorly designed client could bombard your API with an excessive number of requests, consuming all available resources and affecting the experience for other clients. This is where rate limiting comes to the rescue!

In Go, you can implement rate limiting using the golang.org/x/time/rate package, which provides a flexible and easy-to-use API for defining rate limits. The package allows you to specify the maximum number of requests allowed within a given time window. For example, you might decide to allow only 100 requests per minute or 10 requests per second.

To demonstrate how rate limiting works in Go, let's take a look at the code snippet:

package main

import (
	"log"
	"net/http"

	"golang.org/x/time/rate"
)

var requestLimiter = rate.NewLimiter(1, 3)

func rateLimitMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !requestLimiter.Allow() {
			http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", okHandler)

	// Wrap the servemux with the rate limit middleware.
	log.Print("Listening on :4000...")
	http.ListenAndServe(":4000", rateLimitMiddleware(mux))
}

func okHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("OK"))
}

In the code, we create a rate limiter using rate.NewLimiter(1, 3). This sets a limit of allowing 1 request per second, with a burst capacity of 3 requests. Burst capacity means that if there are fewer than 3 requests in a second, they can be accumulated and processed later.

The limit function is a middleware that wraps our API handler. When a request comes in, the middleware first checks if the rate limiter allows the request. If the request exceeds the allowed rate, the middleware responds with a "429 Too Many Requests" status code, indicating that the client should slow down. Otherwise, the request is passed along to the next handler.

You can customize rate limiting based on your specific needs, adjusting the rate and burst capacity values to strike the right balance between allowing sufficient traffic and protecting your system.

So, whether you're building a high-traffic API or want to safeguard your application from potential abuse, rate limiting in Go is a valuable technique to maintain stability, performance, and security. It's like having a responsible bouncer for your application, keeping things running smoothly and preventing unruly behavior.

Rate limiting on API calls

Rate limiting on API calls per user is a common technique used to control the number of requests a user can make to an API within a specific time period. It helps prevent abuse, protect the server from being overwhelmed, and maintain fair usage among all users.

In the given code snippet, the original implementation focused on rate limiting based on the user's IP address. However, let's explore a different use case: rate limiting per user based on some identifier like an API key. This approach allows you to set different rate limits for different users, providing more flexibility and customization.

To implement rate limiting per user, we can create a map to store rate limiters for each user, using the user identifier as the map key. In this updated scenario, instead of relying on the stability of the keys, we'll use a traditional map protected by a mutex for better performance.

The getVisitor function retrieves the rate limiter for the current user. It locks the mutex to ensure thread safety, checks if the rate limiter already exists for the user, and if not, creates a new rate limiter with a specified rate (e.g., 1 request per 3 seconds) and adds it to the map. Finally, it returns the rate limiter for further usage.

The limit middleware is responsible for applying rate limiting to incoming API requests. It retrieves the user identifier (e.g., API key) and calls the getVisitor function to obtain the corresponding rate limiter. If the rate limiter indicates that the user has exceeded the allowed rate (by returning false from Allow()), it returns a 429 (Too Many Requests) HTTP response to indicate that the user has reached their limit. Otherwise, it allows the request to proceed by calling the next handler.

It's important to note that this implementation is a simplified example and doesn't cover all aspects of rate limiting. In a real-world scenario, you might need to consider factors such as different rate limits for different API endpoints, storing rate limit data persistently, handling rate limit resets, and providing appropriate feedback to users when they exceed their limits.

By implementing rate limiting per user, you can effectively control the usage of your API, prevent abuse or excessive requests from individual users, and ensure fair resource allocation among all users.

package main

import (
	"log"
	"net/http"
	"sync"

	"golang.org/x/time/rate"
)

var endpoints = make(map[string]*rate.Limiter)
var mu sync.Mutex

func getEndpointLimiter(endpoint string) *rate.Limiter {
	mu.Lock()
	defer mu.Unlock()

	limiter, exists := endpoints[endpoint]
	if !exists {
		limiter = rate.NewLimiter(10, 20) // 10 requests per 20 seconds
		endpoints[endpoint] = limiter
	}

	return limiter
}

func limit(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Get the requested endpoint.
		endpoint := r.URL.Path

		// Call the getEndpointLimiter function to retrieve the rate limiter for the current endpoint.
		limiter := getEndpointLimiter(endpoint)
		if !limiter.Allow() {
			http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func main() {
	// Example usage
	http.Handle("/", limit(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})))

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Authentication

One of the simplest ways to protect your application is by using middleware. In this particular middleware, we want to achieve three main objectives:

  • Extract the username and password from the request's Authorization header, if it exists. Fortunately, Go provides the handy r.BasicAuth() method introduced in Go 1.4, which makes this extraction process straightforward.
  • Compare the provided username and password against the expected values. To ensure security and prevent timing attacks, we should use Go's subtle.ConstantTimeCompare() function for the comparison. This function ensures that the comparison always takes the same amount of time, regardless of whether the strings match or not. It mitigates the risk of an attacker exploiting timing discrepancies to determine the correct username and password.

It's worth noting that the regular == comparison operator in Go would return as soon as it detects a difference between two strings. This behavior could potentially expose your application to timing attacks. By using subtle.ConstantTimeCompare(), we can protect against this risk.

Additionally, to prevent information leakage about the length of the username and password, we should hash both the provided and expected values using a fast cryptographic hash function like SHA-256 before performing the comparison. This ensures that the lengths of the strings being compared are equal and avoids early returns in subtle.ConstantTimeCompare().

  • Handle the response based on the comparison result. If the username and password are not correct, or the request doesn't contain a valid Authorization header, the middleware should send a 401 Unauthorized response. To inform the client that basic authentication should be used to gain access, we set the WWW-Authenticate header. On the other hand, if the provided credentials are valid, the middleware allows the request to proceed and calls the next handler in the chain.

You might be wondering about the realm value and why we set it to "restricted" in the WWW-Authenticate response header. The realm value allows you to create partitions of protected space in your application. For example, you can have different realms for "documents" and "admin area," each requiring separate credentials. By setting the realm, a web browser or client can cache and automatically reuse the same username and password for requests within the same realm, reducing the need for repeated authentication prompts.

In the provided code snippet, we set the realm value to a hardcoded string, "restricted," since we don't require multiple partitions in this case.

For better security and flexibility, it's recommended to store the expected username and password values in environment variables or pass them as command-line flag values when starting the application, rather than hard-coding them into your codebase. This approach allows you to change the credentials without modifying the application's source code.

By implementing this middleware, you can add a layer of authentication to your application, ensuring that only authorized users can access protected resources. It helps guard against unauthorized access attempts and enhances the overall security of your application.

Putting that together, the pattern for implementing some middleware looks like this:

func basicAuth(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Extract the username and password from the request
		// Authorization header. If no Authentication header is present
		// or the header value is invalid, then the 'ok' return value
		// will be false.
		username, password, ok := r.BasicAuth()
		if !ok {
			// If the Authentication header is not present or invalid,
			// set the WWW-Authenticate header and send a 401 Unauthorized response.
			w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// Calculate SHA-256 hashes for the provided and expected
		// usernames and passwords.
		providedUsernameHash := sha256.Sum256([]byte(username))
		providedPasswordHash := sha256.Sum256([]byte(password))
		expectedUsernameHash := sha256.Sum256([]byte("your expected username"))
		expectedPasswordHash := sha256.Sum256([]byte("your expected password"))

		// Use subtle.ConstantTimeCompare to compare the hashes and
		// check if the provided username and password match the expected ones.
		usernameMatch := subtle.ConstantTimeCompare(providedUsernameHash[:], expectedUsernameHash[:]) == 1
		passwordMatch := subtle.ConstantTimeCompare(providedPasswordHash[:], expectedPasswordHash[:]) == 1

		// If the username and password are correct, call the next handler in the chain.
		if usernameMatch && passwordMatch {
			next.ServeHTTP(w, r)
			return
		}

		// If the username or password is wrong, set the WWW-Authenticate header
		// and send a 401 Unauthorized response.
		w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
	})
}