Table of Contents
In this tutorial we will go step by step and create a simple web server using golang. Additionally I will show you the steps to
1. Create Simple Web Server and Client (Hello World)
Here is a small golang program which sets up a web server that listens on port 8000 and serves a simple "Hello, World!" message when a request is made to the root path ("/").
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8000", nil)
}
The http.HandleFunc
function is used to register a function to be called when a request is made to the root path ("/"). The function takes an http.ResponseWriter
and an *http.Request
as arguments, and writes "Hello, World!" to the response writer.
The http.ListenAndServe
function is used to start the web server. It takes a port number and a handler as arguments. In this case, we're passing nil as the handler because we have already registered our function using http.HandleFunc
.
Execute the program:
# go run main.go
Now from a different terminal, try to access this web server:
# curl http://localhost:8000
Hello, World!
Instead of using curl, we can also write a simple golang client to connect to the web server and print the response status:
package main
import (
"fmt"
"net/http"
)
func main() {
res, err := http.Get("http://localhost:8000/")
if err != nil {
fmt.Println("Error:", err)
return
}
defer res.Body.Close()
fmt.Println("Response:", res.Status)
}
Let's execute the client.go
file:
# go run client.go
Response: 200 OK
2. Add timeout to your web server
We can also define a read and write timeout for our web server, here is an example code:
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
server := &http.Server{
Addr: ":8000",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server.ListenAndServe()
}
In this example:
- The
http.NewServeMux
function is used to create a new request multiplexer. - The
mux.HandleFunc("/", ...)
function is used to register a function as the handler for all requests to the root path ("/
"). - The
http.Server{...}
struct is used to configure the server. - The
server.Addr
field is set to ":8000" to listen on port 8000. - The
server.Handler
field is set to the multiplexer mux created earlier. - The
server.ReadTimeout
field is set to 5 seconds to timeout read operations after 5 seconds. - The
server.WriteTimeout
field is set to 10 seconds to timeout write operations after 10 seconds. - The
server.ListenAndServe()
function is used to start the web server and handle incoming requests using the registered handler.
3. Serve index.html
file with your web server
We can use http.ServeFile
function to serve a specific file. Here's an example to server index.html
file which is available inside web/index.html
on my setup:
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "web/index.html")
})
http.ListenAndServe(":8000", nil)
}
This is my sample index.html
file:
<!DOCTYPE html>
<html>
<head>
<title>My Web Page</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
Execute the code to start the web server:
# go run main.go
Now try to access your web server using browser or any CLI command such as curl
:
3. Create simple web server to download files
If we have a requirement to setup a web server which can be used by users to download files, then you can check below example:
package main
import (
"net/http"
)
func main() {
http.Handle("/files/", http.StripPrefix("/files/", http.FileServer(http.Dir("downloads"))))
http.ListenAndServe(":8000", nil)
}
Here,
- The
http.StripPrefix("/files/", ...)
function is used to remove the "/files/
" prefix from the request URL before passing it to the file server. - The
http.FileServer(http.Dir("downloads"))
function creates a file server that serves files from the "downloads
" directory located in the same directory of the go file. http.Handle("/files/", ...)
is used to register the file server as the handler for all requests that start with "/files/
".- The
http.ListenAndServe(":8000", nil)
function is used to start the web server on port8000
and handle incoming requests using the registered handler. - Now when a client requests the
http://localhost:8000/files/somefile.txt
the server will look for the somefile.txt in the downloads directory and serve it to the client for download.
Here is my downloads
directory content which is located in the same path as my go file:
# tree downloads/ downloads/ ├── config.yaml ├── data.json ├── file.csv ├── file.txt ├── file.xml ├── go.mod ├── go.sum ├── main.go ├── script.sh └── source_file.txt 0 directories, 10 files
Let's start our web server:
$ go run main.go
Now access the web server on GUI and verify the files are available for download:
We can also enhance the code to print a message on the console with the filename which was downloaded, here is the updated code:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/files/", func(w http.ResponseWriter, r *http.Request) {
fileName := r.URL.Path[len("/files/"):]
fmt.Println("Downloading file: " + fileName)
http.ServeFile(w, r, "downloads/"+fileName)
})
http.ListenAndServe(":8000", nil)
}
With this you can re-start your web server and now if we try to download any file then a message is printed on the console:
# go run main.go Downloading file: go.mod Downloading file: file.csv ^Csignal: interrupt
4. Create a web server and client using HTTPS
Example-1: Using self-signed certificates (Without CA Cert)
In this section we will create self-signed certificates and use them to secure our web server. In production environment it is recommended to use third party CA signed certificates instead of self-signed certificates as there will no client or server level validation here. We will still have to use insecure flag to perform any kind of communication with this web server.
Use the following command to generate self-signed certificate for localhost web server:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'
This will generate cert.pem
and key.pem
file.
Here is the updated code for our web server to use this certificate:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServeTLS(":8000", "cert.pem", "key.pem", nil)
}
Start the web server:
# go run main.go
Let's try to access this using curl:
# curl https://localhost:8000/
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
As expected, curl is reporting validation errors. To overcome this we have to use --insecure
flag:
# curl --insecure https://localhost:8000/
Hello, World!
Here is a sample client.go which uses tls.Config{InsecureSkipVerify: true}
to ignore certificate validation errors.
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
res, err := client.Get("https://localhost:8000/")
if err != nil {
fmt.Println("Error:", err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
fmt.Println("Response:", res.Status)
fmt.Println("Text received:", string(body))
}
Let's execute this client code:
# go run client.go
Response: 200 OK
Text received: Hello, World!
Example-2: Using CA signed certificate
If you have a valid CA certificate then you can use that to sign a server certificate and then use them with your web server. You can follow our certificate management articles to learn all the tips and tricks related to generating different types of certificates.
I am using following script and file to generate the CA certificate and other certificates required for our secure web server. For the sake of this article, I will not be able to explain the steps but you can read the original article from where I am using this script:
#!/bin/bash
myPath="/root/goexamples"
SERVER_KEY="$myPath/server.key"
SERVER_CSR="$myPath/server.csr"
SERVER_CRT="$myPath/server.crt"
CA_KEY="$myPath/ca.key"
CA_CRT="$myPath/cacert.pem"
CA_EXTFILE="$myPath/ca_cert.cnf"
SERVER_EXT="$myPath/server_ext.cnf"
SERVER_CONF="$myPath/server_cert.cnf"
OPENSSL_CMD="/usr/bin/openssl"
COMMON_NAME="$1"
function generate_root_ca {
## generate rootCA private key
echo "Generating RootCA private key"
if [[ ! -f $CA_KEY ]];then
$OPENSSL_CMD genrsa -out $CA_KEY 4096 2>/dev/null
[[ $? -ne 0 ]] && echo "ERROR: Failed to generate $CA_KEY" && exit 1
else
echo "$CA_KEY seems to be already generated, skipping the generation of RootCA certificate"
return 0
fi
## generate rootCA certificate
echo "Generating RootCA certificate"
$OPENSSL_CMD req -new -x509 -days 3650 -config $CA_EXTFILE -key $CA_KEY -out $CA_CRT 2>/dev/null
[[ $? -ne 0 ]] && echo "ERROR: Failed to generate $CA_CRT" && exit 1
## read the certificate
echo "Verify RootCA certificate"
$OPENSSL_CMD x509 -noout -text -in $CA_CRT >/dev/null 2>&1
[[ $? -ne 0 ]] && echo "ERROR: Failed to read $CA_CRT" && exit 1
}
function generate_server_certificate {
echo "Generating server private key"
$OPENSSL_CMD genrsa -out $SERVER_KEY 4096 2>/dev/null
[[ $? -ne 0 ]] && echo "ERROR: Failed to generate $SERVER_KEY" && exit 1
echo "Generating certificate signing request for server"
$OPENSSL_CMD req -new -key $SERVER_KEY -out $SERVER_CSR -config $SERVER_CONF 2>/dev/null
[[ $? -ne 0 ]] && echo "ERROR: Failed to generate $SERVER_CSR" && exit 1
echo "Generating RootCA signed server certificate"
$OPENSSL_CMD x509 -req -in $SERVER_CSR -CA $CA_CRT -CAkey $CA_KEY -out $SERVER_CRT -CAcreateserial -days 365 -sha512 -extfile $SERVER_EXT 2>/dev/null
[[ $? -ne 0 ]] && echo "ERROR: Failed to generate $SERVER_CRT" && exit 1
echo "Verifying the server certificate against RootCA"
$OPENSSL_CMD verify -CAfile $CA_CRT $SERVER_CRT >/dev/null 2>&1
[[ $? -ne 0 ]] && echo "ERROR: Failed to verify $SERVER_CRT against $CA_CRT" && exit 1
}
# MAIN
generate_root_ca
generate_server_certificate
This script requires some additional configuration files namely server_cert.cnf
, ca_cert.cnf
and server_ext.cnf
to automate the process:
# server_cert.cnf
default_bit = 4096
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
countryName = IN
stateOrProvinceName = Karnataka
localityName = Bengaluru
organizationName = GoLinuxCloud
commonName = example.com
# server_ext.cnf
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
# ca_cert.cnf
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[req_distinguished_name]
countryName = IN
stateOrProvinceName = Karnataka
localityName = Bengaluru
organizationName = GoLinuxCloud
commonName = rootca.com
[ v3_ca ]
basicConstraints=critical,CA:TRUE
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
Execute the script:
# ./gen_certs.sh
Generating RootCA private key
Generating RootCA certificate
Verify RootCA certificate
Generating server private key
Generating certificate signing request for server
Generating RootCA signed server certificate
Verifying the server certificate against RootCA
Let's verify our certificates:
[root@server goexamples]# ls -l server* -rw-r--r-- 1 root root 279 Jan 21 16:55 server_cert.cnf -rw-r--r-- 1 root root 2102 Jan 21 16:57 server.crt -rw-r--r-- 1 root root 1691 Jan 21 16:57 server.csr -rw-r--r-- 1 root root 242 Jan 21 16:55 server_ext.cnf -rw------- 1 root root 3272 Jan 21 16:57 server.key [root@server goexamples]# ls -l ca* -rw-r--r-- 1 root root 403 Jan 21 16:55 ca_cert.cnf -rw-r--r-- 1 root root 2017 Jan 21 16:57 cacert.pem -rw------- 1 root root 3272 Jan 21 16:57 ca.key
Now we will use the server.crt
and server.key
with our web server:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServeTLS(":8000", "server.crt", "server.key", nil)
}
Start the web server:
# go run main.go
Now we can use curl
command to query this web server by specifying the CA certificate which was used to sign the certificate:
# curl --cacert cacert.pem https://localhost:8000
Hello, World!
We can also create golang based client to query this web server. Here you can notice that instead of using InsecureSkipVerify: true
we are use our CA certificate to establish the communication.
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
caCert, err := ioutil.ReadFile("cacert.pem")
if err != nil {
fmt.Println("Error reading CA certificate:", err)
return
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get("https://localhost:8000/")
if err != nil {
fmt.Println("Error:", err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
fmt.Println("Response:", res.Status)
fmt.Println("Text received:", string(body))
}
In this example:
- The
ioutil.ReadFile("cacert.pem")
is used to read the CA certificate from the file. - The
x509.NewCertPool()
is used to create a new certificate pool andcaCertPool.AppendCertsFromPEM(caCert)
is used to add the CA certificate to the pool - The
http.Transport{...}
is used to create an HTTP transport with the custom tls.Config which includes the RootCAs. - The
http.Client{...}
is used to create an HTTP client with the custom transport. - The
client.Get(...)
is used to send a GET request to the server using the custom client.
Let's execute our client code:
# go run client.go Response: 200 OK Text received: Hello, World!
So we are getting 200 OK response which means the communication is working between client and server.
Summary
Here is a summary of what we have learned in this tutorial:
- To set up a simple web server in Go, we can use the
http.ListenAndServe
orhttp.ListenAndServeTLS
functions, along withhttp.HandleFunc
orhttp.Handle
to handle different routes and requests. - To add a timeout to a web server, we can use the
http.Server.ReadTimeout
andhttp.Server.WriteTimeout
fields to set the timeout for reading and writing. - To connect to a server using a self-signed certificate, you can use the
http
package with a customtls.Config
that has theInsecureSkipVerify=true
or alternatively we can use RootCAs to specify the root CA Certificate which can used to do the validation.
References
net/http - Go Packages
Golang. What to use? http.ServeFile(..) or http.FileServer(..)?