Hello! Welcome to your next lesson in the "Interacting with Web Services and APIs" module.
In our last session, we successfully parsed JSON API responses into native Go structs, turning raw data into a structured, usable format. You're now able to make requests and understand the data you get back.
Today, we'll address a critical piece of the puzzle: authentication. Most APIs you'll use in a DevOps context aren't open to the public; they require you to prove your identity. We will learn how to pass authentication tokens and other custom headers in your API requests. This skill is fundamental for automating interactions with platforms like GitHub, GitLab, cloud providers, and other secured services.
1. A Quick Refresher on HTTP Headers
Before we dive into the Go implementation, let's briefly review the role of HTTP headers. They are key-value pairs sent with every HTTP request and response, carrying metadata that tells the client and server how to handle the communication.
Supercharge Your REST API with These Essential HTTP Headers
To start, let's watch a quick overview of what HTTP headers are and why they are important. This video from ByteMonk gives a clear, high-level summary.
Watch from the beginning to 01:08. Focus on understanding the basic structure and purpose of headers as metadata for an HTTP message.
While many headers exist, our primary focus today is on the Authorization header, which is the standard way to carry credentials for API access.
Supercharge Your REST API with These Essential HTTP Headers
The same video provides a concise explanation of the Authorization header.
Now, watch from 06:13 to 07:00. This section explains the header's role and introduces the Bearer token scheme, a common authentication method you'll frequently encounter when working with REST APIs.
The Bearer scheme is simple: you present a token, and the server "bears" (accepts) it as proof of your identity and permissions, without needing complex cryptographic handshakes for every request.
2. Building a Request with Custom Headers in Go
So far, we've used convenience functions like http.Get(). To add headers, we need more control. This involves creating the request object ourselves before sending it. The process is:
- Create a request object using
http.NewRequest(). - Add headers to the request's
Headerfield. - Send the request using an
http.Client.
The req.Header field on an *http.Request object has a Set(key, value) method that makes adding headers straightforward.
How To Make HTTP Requests in Go
The tutorial "How To Make HTTP Requests in Go" from DigitalOcean has a great FAQ section that provides a clear, practical code snippet for adding multiple headers.
Please read the answer to question #3, "How do I add headers to an HTTP request in Go?". Notice how req.Header.Set() is used multiple times to add different kinds of headers: Authorization for security, User-Agent for client identification, Accept for content negotiation, and a custom X-Custom-Header for application-specific information.
As you can see, the pattern is simple and repeatable for any header you need to add.
3. A DevOps Best Practice: Managing API Tokens Securely
Your experience with scripting and automation has surely taught you that hardcoding secrets like API keys or tokens into your code is a significant security risk. A more secure and flexible approach is to load them from the environment.
Go's standard library os package provides the Getenv() function for this purpose. This is the idiomatic way to handle secrets in Go applications, allowing your tool to be configured for different environments (dev, staging, prod) without changing the code.
Configuring the Go HTTP client
Let's look at a resource that combines the act of setting the Authorization header with the best practice of loading the token from the environment. The article "Configuring the Go HTTP client" from LogRocket demonstrates this perfectly.
Read the section titled "Authorizing your requests". The key takeaway here is the use of os.Getenv("TOKEN") to retrieve the secret. This decouples your code from the sensitive value, which is crucial for building secure and maintainable automation tools.
4. Putting It All Together: A Practical Example
Let's combine what we've learned into a practical program. We will write a small Go tool that fetches your user profile from the GitHub API. This requires authentication, so it's a perfect use case.
Here's the plan for our program:
- Define a
structto hold the GitHub user data (connecting to our last lesson). - Read a GitHub Personal Access Token from an environment variable named
GITHUB_TOKEN. - Create an
http.Clientwith a reasonable timeout. - Construct a new
GETrequest to the GitHub API. - Set the
Authorizationheader with the token. - Set a custom
User-Agentheader, which is a good practice (and required by the GitHub API). - Execute the request, check for errors and non-200 status codes.
- Parse the JSON response into our struct and print the result.
Here is the complete code. You can save this as main.go.
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
// GitHubUser represents a subset of the fields from the GitHub /user API response.
type GitHubUser struct {
Login string `json:"login"`
Name string `json:"name"`
PublicRepos int `json:"public_repos"`
}
func main() {
// 1. Read the token from an environment variable.
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
fmt.Println("Error: GITHUB_TOKEN environment variable not set.")
os.Exit(1)
}
// 2. Create a custom HTTP client.
client := &http.Client{
Timeout: 10 * time.Second,
}
// 3. Create the request object.
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
os.Exit(1)
}
// 4. Set the required headers.
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "Go-DevOps-Tool/1.0")
// GitHub API recommends this header for API versioning.
req.Header.Set("Accept", "application/vnd.github.v3+json")
// 5. Execute the request.
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error making request: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
// 6. Check for a non-successful status code.
if resp.StatusCode != http.StatusOK {
fmt.Printf("Error: received non-200 status code: %d\n", resp.StatusCode)
os.Exit(1)
}
// 7. Decode the JSON response.
var user GitHubUser
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
fmt.Printf("Error decoding JSON: %v\n", err)
os.Exit(1)
}
// 8. Print the result.
fmt.Printf("Successfully fetched data for user: %s (%s)\n", user.Name, user.Login)
fmt.Printf("Public Repositories: %d\n", user.PublicRepos)
}
To run this code:
- Generate a GitHub Personal Access Token with the
read:userscope. - Set the environment variable and run the program from your terminal:
export GITHUB_TOKEN="your_github_token_here" go run main.go
You should see your GitHub profile information printed to the console.
Test your understanding!
Imagine you are working with an internal company API that uses a custom header X-Request-ID for tracing requests through different services. How would you modify the main function in the example above to include a header X-Request-ID with the value devops-tool-12345?
Show answer
You would add another req.Header.Set() call after setting the other headers. The new line would look like this:
// Add this line inside the main function, for example, after setting the 'Accept' header.
req.Header.Set("X-Request-ID", "devops-tool-12345")
This demonstrates that adding any custom header follows the exact same pattern.
Conclusion
You have now mastered a critical skill for building powerful DevOps tools: authenticating with and customizing requests to modern APIs. By constructing requests manually and setting headers, you can interact with virtually any secured REST API.
Key Takeaways:
- Use
http.NewRequest()to create a request object that you can customize before sending. - Modify request headers using the
req.Header.Set("Key", "Value")method. - The
Authorization: Bearer <token>header is the standard for token-based authentication. - Always load secrets like API tokens from the environment using
os.Getenv()rather than hardcoding them. - Setting a descriptive
User-Agentheader is a good practice for identifying your tool's API traffic.
Next Up
Our requests are becoming more robust, but what happens when a network is slow or an API is temporarily unavailable? A simple timeout isn't always enough. In our next lesson, we will explore Go's context package to manage request lifecycles and implement timeouts and cancellation, giving you fine-grained control over your network operations.