Web Services Tutorial

Purpose and Scope

This page orients Go developers who are building web services around the standard-library pieces that sit underneath most Go HTTP examples. The official web-service tutorial uses the Gin framework to route requests, retrieve request details, and marshal JSON responses, but the repository-backed primitives are still the net/http server, client, handler, and test-server APIs. Understanding those primitives helps you read framework examples, write smaller services without a framework, and test web handlers with the same HTTP behavior that production clients and servers use.

A web service in this context is a Go program that accepts HTTP requests, dispatches them to handler code, and returns HTTP responses. The core server side is implemented in net/http, while client behavior is represented by http.Client and its request execution path. End-to-end tests can use net/http/httptest, which provides an HTTP server intended specifically for tests and a matching client configured to reach that server. Sources: src/net/http/server.go, src/net/http/client.go, src/net/http/httptest/server.go

Relevant Source Files

  • src/net/http/server.go - Defines the standard-library HTTP server surface, including server configuration, request serving, handlers, response writing, and the package-level helpers that most examples build on.
  • src/net/http/client.go - Defines the high-level http.Client interface, its redirect, cookie, timeout, and transport behavior, and the guidance that clients are safe for concurrent reuse.
  • src/net/http/httptest/server.go - Defines httptest.Server, including NewTestServer, Server.Client, in-memory and loopback networking modes, cleanup behavior, and test-focused configuration rules.

Core Primitives

The central server-side abstraction is the handler: code that receives an HTTP request and writes an HTTP response. Frameworks such as Gin often add routing, parameter binding, middleware, and JSON conveniences, but they ultimately exist in the same ecosystem as net/http request serving. When you design endpoints, start by separating the resource shape from the transport mechanics: decide which paths and methods your API supports, then implement handler functions that validate input, call application logic, and encode responses.

The client-side abstraction is http.Client. The source comments describe Client as the high-level HTTP client interface whose zero value, DefaultClient, is usable with DefaultTransport. A client is higher-level than a RoundTripper because it handles details such as cookies and redirects in addition to sending individual requests. The repository guidance is important for services and tests alike: because transports typically cache TCP connections and have internal state, clients should be reused instead of constructed for every request, and they are safe for concurrent use by multiple goroutines. Sources: src/net/http/client.go

http.Client also defines operational behavior that affects service integrations. Transport specifies how individual requests are made, CheckRedirect customizes redirect policy, Jar manages cookies across outbound requests and inbound responses, and Timeout bounds the whole request lifecycle, including connection time, redirects, and reading the response body. The default redirect policy stops after ten consecutive requests when no custom CheckRedirect is supplied, while sensitive headers are not forwarded to untrusted redirect targets. Those details matter when your web service calls another HTTP API or when your tests assert redirect and cookie behavior. Sources: src/net/http/client.go

The test primitive is httptest.Server. It is described as an HTTP server for end-to-end HTTP tests, with most tests expected to create one through NewTestServer. The server exposes a Client method that returns an http.Client configured to send requests to the test server. This keeps handler tests close to real HTTP semantics without forcing every test to allocate a public port or duplicate client configuration. It also provides a clear seam between handler code and the network behavior that the standard library already implements. Sources: src/net/http/httptest/server.go

Tutorial Flow: From Endpoint Design to Running Code

A practical web service begins with endpoint design. The official REST tutorial frames the first task as designing endpoints for a small API, then creating code, data, and handlers that return all items, add an item, and return a specific item. In a standard-library version of that workflow, you would define paths such as /albums and /albums/{id}, choose methods such as GET and POST, and connect those paths to handlers. Each handler should be responsible for one HTTP interaction: read request data, decide the response status, and write the response body.

After designing endpoints, implement handlers using net/http concepts rather than mixing transport concerns into domain code. For example, a GET /albums handler can read from an in-memory slice or storage layer and encode JSON to the response. A POST /albums handler can decode request JSON, validate it, append or persist a new item, and return an appropriate status. Even if a framework supplies the router, the same division of work applies: routing selects a handler, the handler translates HTTP to application calls, and application code remains independent enough to test directly.

Run the service through the Go toolchain as an ordinary module-backed command. A minimal service can live in a main package, import net/http, register routes, and start listening. In development, keep the command small and push business logic into packages that can be tested without a live server. When the service needs to call another HTTP API, create or reuse an http.Client at the application boundary rather than inside every helper function. That matches the repository documentation that clients are concurrent-safe and should be reused because of cached connection state. Sources: src/net/http/client.go

package main
 
import (
    "encoding/json"
    "log"
    "net/http"
)
 
type album struct {
    ID     string `json:"id"`
    Title  string `json:"title"`
    Artist string `json:"artist"`
}
 
var albums = []album{{ID: "1", Title: "Blue Train", Artist: "John Coltrane"}}
 
func getAlbums(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(albums)
}
 
func main() {
    http.HandleFunc("/albums", getAlbums)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Testing Web Services with httptest

Use httptest.Server when you want to test the behavior visible to an HTTP client rather than only the handler function in isolation. The server documentation distinguishes two modes. The recommended path is NewTestServer, which can use an in-memory network implementation. In that mode, the client returned by Server.Client directs HTTP and HTTPS requests to the server regardless of destination address or hostname, and tests do not need to use Server.URL as the base URL. This avoids port exhaustion and transient networking failures that can make test suites flaky. Sources: src/net/http/httptest/server.go

For tests that need a loopback address, call Server.Start or Server.StartTLS. In loopback mode, the server listens on a system-chosen local port, and Server.URL is set to a base URL containing the server address. The same documentation warns that server configuration must be changed before the first call to Server.Client, Server.Start, or Server.StartTLS. It also notes that servers created by NewTestServer register cleanup and fail the test if the handler panics with an unexpected value, while servers created in other ways must be closed manually. Sources: src/net/http/httptest/server.go

func TestGetAlbums(t *testing.T) {
    server := httptest.NewTestServer(t, http.HandlerFunc(getAlbums))
    resp, err := server.Client().Get("http://example.com/albums")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK)
    }
}

This style complements Go's ordinary test conventions. A file ending in _test.go is discovered by go test, test functions are named with the TestName form, and the testing.T value reports failures. For web services, that means you can first unit-test pure application logic, then add handler tests with httptest for request parsing and response encoding, and finally reserve full integration tests for cases involving external dependencies. The repository test server helps keep the middle layer realistic without turning every handler test into a deployment exercise.

System-to-Code Mapping

Reader taskStandard-library componentRepository source
Accept HTTP requests and dispatch to handlersnet/http server, handler, and response-writing APIssrc/net/http/server.go
Call another HTTP service from Go codehttp.Client, RoundTripper, redirects, cookies, timeoutsrc/net/http/client.go
Exercise handlers through real HTTP semantics in testshttptest.Server, NewTestServer, Server.Client, Start, StartTLSsrc/net/http/httptest/server.go

The important architectural boundary is that server handlers and clients are independent but composable. A handler can receive a request from a production server, from a framework adapter, or from httptest.Server; the handler code should not need to know which one invoked it. A client can call a production URL, a loopback test URL, or an in-memory test server, provided the test server's configured client is used. This symmetry is what makes Go web services straightforward to develop incrementally: start with handlers, add clients where external calls are required, and use test servers to validate the HTTP contract.

Next Steps

Build the first endpoint with net/http, then add one handler at a time for each resource and method in your API design. Reuse http.Client instances for outbound calls, set explicit timeouts for service-to-service requests, and keep redirect and cookie behavior in mind when testing authentication or cross-host flows. Add _test.go files that use httptest.NewTestServer for end-to-end handler behavior, and use loopback mode only when the test specifically needs a real local address. For broader workflow context, continue with the testing, database access, and go test pages.