There’s too much going on in main.go
.
Let’s refactor the code so that main.go
contains no wttr logic.
In keeping with Go naming conventions I’m going to call the client
wttrclient
(I know, original).
I want it to have one CurrentWeather()
function to return the weather.
Going top-down lets write the tests:
wttrclient_test.go #
package wttrclient_test
import (
"testing"
"github.com/alrayyes/gwttr/wttrclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIClient_CanGetTheCurrentWeather(t *testing.T) {
client := wttrclient.NewWTTRClient()
got, err := client.CurrentWeather(t.Context())
require.NoError(t, err)
want := "Weather report: honolulu"
assert.Contains(t, got, want)
}
We need a NewWTTRClient()
function to create a new wttrclient
.
wttrclient.CurrentWeather()
should return a string containing Weather report: honolulu
.
The logic should pretty much write itself at this point.
wttrclient.go #
// Package wttrclient provides a client to access https://wttr.in.
package wttrclient
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
const timeout = 5 * time.Second
const url = "https://wttr.in/honolulu?0A"
// WTTRClient provides a client to access https://wttr.in
type WTTRClient struct {
client http.Client
}
// CurrentWeather returns the current weather for Honolulu.
func (w *WTTRClient) CurrentWeather(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
url,
nil,
)
if err != nil {
return "", fmt.Errorf("could not create request: %w", err)
}
resp, err := w.client.Do(req)
if err != nil {
return "", fmt.Errorf("could not do request: %w", err)
}
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("could not read response: %w", err)
}
return string(bytes), nil
}
// NewWTTRClient creates a new WTTRClient instance.
func NewWTTRClient() WTTRClient {
client := WTTRClient{
client: http.Client{
Timeout: timeout,
},
}
return client
}
Creating the wttrclient #
// NewWTTRClient creates a new WTTRClient instance.
func NewWTTRClient() WTTRClient {
client := WTTRClient{
client: http.Client{
Timeout: timeout,
},
}
return client
}
What we’re doing here is creating a new WTTRClient
and instantiating a new http.client.
This is the preferred golang way to do http things.
In the previous version, this was hidden away by http.Get.
The timeout is also passed here as an http.client
attribute.
Creating the request #
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
url,
nil,
)
if err != nil {
return "", fmt.Errorf("could not create request: %w", err)
}
Create a new request here. You’ll notice that we use http.NewRequestWithContext so we can pass the context. This gives us multiple benefits:
- Context Support for Cancellation and Timeouts:
- The primary reason to use http.NewRequestWithContext is to associate a context.Context with the HTTP request. This allows you to control the request’s lifecycle, including cancellation, timeouts, and deadlines. For example, if the request takes too long or the user cancels the operation, the context can be used to abort the request gracefully.
- Better Resource Management:
- By using a context, you ensure that resources (like network connections) are properly cleaned up if the request is canceled or times out. This prevents resource leaks and improves the robustness of your application.
- Standardization and Future-Proofing:
- The Go community has embraced the use of context.Context for managing request lifecycles. Using http.NewRequestWithContext aligns with this best practice and ensures your code is consistent with modern Go patterns.
- Integration with Other Context-Aware APIs:
- Many libraries and frameworks in Go are context-aware. By using http.NewRequestWithContext, you can easily integrate your HTTP requests with other context-aware operations, such as database queries or gRPC calls.
The go blog provides a good explanation of what’s possible with context.
If you’re a beginner you don’t need to worry about any this, just know that it’s best to use http.NewRequestWithContext
.
Send the request and handling the response #
Nothing really changes from the previous version here, so don’t worry about it.
main.go #
func main() {
client := wttrclient.NewWTTRClient()
weather, err := client.CurrentWeather(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println(weather)
}
main()
is nice and thin now, just the way I like it! The main test is essentially an e2e test now (It kind of always was one), so we don’t need to change anything there.
Source code #
This version of the code can be found here.
Proof of concept go wttr client