Building a Go Web Server from Scratch
James Reed
Infrastructure Engineer · Leapcell

Several Ways to Write a Web Server in Go
I. The Difference between a Web Server and an HTTP Server
An HTTP Server, as understood from its name, is a server that supports the HTTP protocol. While a Web Server, in addition to supporting the HTTP protocol, may also support other network protocols. This article will focus on introducing several common ways to write a Web Server using the official package of Golang.
II. The Simplest HTTP Server
This is the simplest way to implement a Web Server. The following is the sample code:
package main import ( "fmt" "log" "net/http" ) func myHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello there!\n") } func main() { http.HandleFunc("/", myHandler) // Set the access route log.Fatal(http.ListenAndServe(":8080", nil)) }
After starting this program, you can open another terminal and enter the command curl localhost:8080
, or directly open localhost:8080
in a browser, and you will see the output result Hello there!
.
The function of the ListenAndServe
function is to listen for and handle connections. Its internal processing method is to start a goroutine for each connection to handle it. However, this is not an ideal processing method. Students who have studied operating systems know that the cost of process or thread switching is huge. Although goroutines are user-level lightweight threads and switching them will not cause a switch between user mode and kernel mode, when the number of goroutines is large, the cost of switching is still not negligible. A better way is to use a goroutine pool, but this article will not elaborate on it for now.
III. Using the Handler Interface
The previous method has poor scalability. For example, it is impossible to set the server's timeout. At this time, we can use a custom server.
The Server
structure is defined as follows:
type Server struct { Addr string // TCP address to listen on Handler Handler // handler to invoke ReadTimeout time.Duration // maximum duration before timing out read of the request WriteTimeout time.Duration // maximum duration before timing out write of the response TLSConfig *tls.Config // ... }
The Handler
is an interface, defined as follows:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
So, as long as we implement the ServeHTTP
method of the Handler
interface, we can customize the server. The sample code is as follows:
package main import ( "log" "net/http" "time" ) type myHandler struct{} func (this myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Specific processing logic } func main() { server := http.Server{ Addr: ":8080", Handler: &myHandler{}, ReadTimeout: 3 * time.Second, // ... } log.Fatal(server.ListenAndServe()) }
IV. Directly Handling the conn
Sometimes we need to handle the connection at a more fundamental level, and at this time, we can use the net
package. The server-side code is simply implemented as follows:
package main import ( "log" "net" ) func handleConn(conn net.Conn) { // Specific logic for handling the connection } func main() { listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { // Handle the error } go handleConn(conn) } }
For the convenience of the example, here a goroutine is started for each connection to handle it. In actual use, using a goroutine pool is usually a better choice. For the return information to the client, we just need to write it back to the conn
.
V. Proxy
The previous sections introduced several ways to implement a Web Server. Now, let's simply implement an HTTP proxy. Writing a proxy in Golang is very simple. We just need to forward the connection.
package main import ( "io" "log" "net" ) func handleConn(from net.Conn) { to, err := net.Dial("tcp", ":8001") // Establish a connection with the target server if err != nil { // Handle the error } done := make(chan struct{}) go func() { defer from.Close() defer to.Close() io.Copy(from, to) done <- struct{}{} }() go func() { defer from.Close() defer to.Close() io.Copy(to, from) done <- struct{}{} }() <-done <-done } func main() { listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { // Handle the error } go handleConn(conn) } }
By slightly enhancing the proxy, more functions can be implemented.
Leapcell: The Next-Gen Serverless Platform for Golang app Hosting
Finally, I would like to recommend a platform that is most suitable for deploying Go services: Leapcell
1. Multi-Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ