Vladislav Yarmak
Modern backend development is overly complex. In order to deploy even simplest Golang web service now you’d probably going to need:
- Go’s web server. Even though it likely will be the one from stdlib, do we really need every network-enabled piece of software to carry a web server as a dependency?
- Some router library. Again, there’s good one in stdlib, but are URL paths really a concern of application level?
- CI pipeline to automate build of your application.
- Docker (or maybe even more complex container runtime in production environment).
- Quite often - a separate “frontend” web server to handle static files, rate limits, TLS termination and other infrastructure details.
- A lot of other nonsense to match modern vision of backend infrastructure.
Go makes many things straightforward, yet there’s still a lot of unnecessary complexity. Some companies even have dedicated “platform engineering” teams to deal with that, which speaks volumes about the issue.
Web services with dynamic content is a problem solved long time ago. Does it really need to be that complex today? Is there easier way? I think, there is.
The Right Way
The way I’m suggesting is better explained with illustrated example, going through a complete step-by-step guide.
Step 1. Get yourself a VPS
Just get a Linux server. In this guide I use one with Ubuntu 24.04.
Step 2. Install web server
Let’s go with tried and true Apache 2.
apt update && apt install -y apache2
a2enmod cgid
Edit file /etc/apache2/conf-enabled/serve-cgi-bin.conf and make sure it looks like this:
<IfModule mod_alias.c>
<IfModule mod_cgi.c>
Define ENABLE_USR_LIB_CGI_BIN
</IfModule>
<IfModule mod_cgid.c>
Define ENABLE_USR_LIB_CGI_BIN
</IfModule>
<IfDefine ENABLE_USR_LIB_CGI_BIN>
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
SetEnv GOCACHE /var/tmp/.www-cache/go-build
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Require all granted
</Directory>
</IfDefine>
</IfModule>
Note the added line SetEnv GOCACHE /var/tmp/.www-cache/go-build.
Finally, restart the web server to apply configuration:
systemctl restart apache2
Step 3. Setup support for Go scripts
First we need Golang itself.
Then we need a binfmt support to enable Golang scripts:
go install github.com/Snawoot/go-binfmt@latest
install /root/go/bin/go-binfmt /usr/local/bin
cat > /etc/systemd/system/go-binfmt.service <<'EOF'
[Unit]
Description=Register go-binfmt support
After=proc-sys-fs-binfmt_misc.mount
Wants=proc-sys-fs-binfmt_misc.mount
[Service]
Type=oneshot
ExecStart=/usr/local/bin/go-binfmt -register
ExecStop=/usr/local/bin/go-binfmt -unregister
RemainAfterExit=yes
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now go-binfmt.service
Step 4. Code!
Let’s greet the world with a simple “Hello World” Go script!
Create /usr/lib/cgi-bin/hello.go with following content:
package main
import (
"fmt"
"net/http"
"net/http/cgi"
)
func main() {
err := cgi.Serve(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintln(w, "<!DOCTYPE html>")
fmt.Fprintln(w, "<html><body><head><title>Hello World</title></head>")
fmt.Fprintln(w, "<h1 style=\"color: green;\">Hello, World!</h1>")
fmt.Fprintln(w, "<p>This page was generated by a Go script running on Apache2 (Ubuntu 24.04).</p>")
fmt.Fprintln(w, "</body><html>")
}))
if err != nil {
fmt.Println(err)
}
}
And make this script executable:
chmod +x /usr/lib/cgi-bin/hello.go
That’s it! Now your web application is accessible on URL http://SERVER_IP/cgi-bin/hello.go.
Advantages of This Approach
- Live reload! Web page changes as soon as you modify script file.
- It comes closer to the simplicity and ergonomics of web development with PHP.
- Request routing is no longer your app concern, it’s adjustable in Apache 2 configuration (however, you can access every request properties as usual, if you really need to).
- Logging is no longer your app concern, Apache 2 will do it for you.
- TLS, static files and other details are now just a configuration options for Apache 2.
- No unnecessary complexity, no docker, no compile stage to be concerned about, no integrated web server, no boilerplate code.
- Dynamic resource allocation: your app is not even loaded into memory when not handling a request.
- Some entertainment value for April Fools’ Day!