HTTP Client and Server in Go

Among my favorite features of the Go language is the fact that Go has concurrency and network programming “natively” built in. It reflects Go as being designed for the 21st century, where the network is as much taken for granted as the filesystem was towards the end of the 20th.

As cut-n-paste examples, here are skeleton implementation of both an HTTP server and an HTTP client. These implementations are intentionally bare-bones, so as not to obscure the most relevant points. It should be easy enough to extend them as desired.

HTTP Server

package main

import ( "fmt"
	"net/http" )

func main() {
	PORT := ":8088"                 // As string!
	DOCROOT := "/home/user/www"     // Or whatever. No trailing slash!
	
	// Default endpoint: Serve static files from DOCROOT
	http.HandleFunc( "/",
		func( w http.ResponseWriter, r *http.Request ) {
			path := r.URL.Path      // extract path from request URL
			http.ServeFile( w, r, DOCROOT + path )
		} )

	// "action" endpoint: Return a custom response
	http.HandleFunc( "/action", 	
		func( w http.ResponseWriter, r *http.Request ) {
			result := "Hello, World!"
			fmt.Fprintf( w, result )
		} )

	// "form" endpoint: Parse a form submitted via POST
	http.HandleFunc( "/form",
		func( w http.ResponseWriter, r *http.Request ) {

			// This populates the field r.Form with a map[string][]string
			err := r.ParseForm()
			if err != nil {
				fmt.Println( "Error: Could not parse form", err )
			}

			// Print values
			for key, vals := range( r.Form ) {
				fmt.Printf( "%s : ", key )
				for _, v := range( vals ) {
					fmt.Printf( "%s ", v )
				}
				fmt.Printf( "\n" )
			}

			// Create a return
			fmt.Fprintf( w, "ok" )
		} )

	// Listen on localhost:PORT
	fmt.Println( http.ListenAndServe(PORT, nil) )
}

This server implementation provides three endpoints:

  • / : Serves static files from a (hardcoded) doc root directory.
  • /action : Calculates and returns a custom result; an example of an API endpoint.
  • /form : Parses a form submitted via an HTTP POST request, and prints the contents of the form to standard output.

HTTP Client

package main

import ( "io"
	"log"
	"net/http"
	"net/url"
	"os" )

func main() {
	host := "http://localhost:8088"
	outfile := "/tmp/outfile"

	// -----
	
	// GET
	res, err := http.Get( host )
	if err != nil {
		log.Fatalln( "GET failed", err )
	}
	bytes, err := io.ReadAll( res.Body ) // copy body into []bytes
	res.Body.Close()

	// Save bytes to file
	out, err := os.Create( outfile )
	if err != nil {
		log.Fatalln( "Could not create output file", err )
	}
	_, err = out.Write( bytes )
	if err != nil {
		log.Fatalln( "Coult not write output file", err )
	}

	// -----
	
	// POST
	form := url.Values{}       // create an empty form
	form.Set( "key1", "val1" )
	form.Add( "key2", "val2a" )
	form.Add( "key2", "val2b" )

	res, err = http.PostForm( host + "/form", form )
	if err != nil {
		log.Fatalln( "Could not POST form", err )
	}
}

The client first performs an HTTP GET request, then saves the body of the response to a file. Note that res.Body implements the io.ReadCloser interface: it must be read (into a different data structure, like a slice of bytes, for example), and the closed.

The client then creates and populates a form, which it then submits via HTTP POST to the server, specifying an appropriate endpoint.

Conclusion

This is it, these listings demonstrate most of common HTTP transactions; one can take it from here. Of course, the net/http package provides much more functionality, if needed.

Files