Back to Go Examples

Hosting: Run a Go Web App on Windows

Install Go with winget, build a web server, and run it as a persistent Windows Service using NSSM — no admin scripting required.

Contents

  1. Install Go
  2. Write the Web App
  3. The Complete Code
  4. Build & Smoke-Test
  5. Run as a Windows Service with NSSM
  6. Open the Firewall
  7. Useful Commands

1. Install Go

Open PowerShell or Command Prompt as a regular user and run:

winget install GoLang.Go

Close and reopen your terminal after the install completes so the updated PATH takes effect, then confirm:

go version
go version go1.22.5 windows/amd64

Alternatively, download the .msi installer from go.dev/dl and run it. It adds C:\Program Files\Go\bin to your system PATH automatically.

2. Write the Web App

Create a project directory and initialise a module. You can use PowerShell or Command Prompt:

mkdir %USERPROFILE%\go-apps\hello
cd %USERPROFILE%\go-apps\hello
go mod init hello

The app has four routes:

It listens on :8081 so you can open it in your browser at http://localhost:8081 immediately, no reverse proxy needed.

3. The Complete Code

Save this as %USERPROFILE%\go-apps\hello\main.go:

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync/atomic"
	"time"
)

const listenAddr = ":8081"

var visitCount uint64

const styleCSS = `body{font-family:system-ui,sans-serif;max-width:640px;margin:3rem auto;padding:0 1rem;color:#222}
h1{color:#06c}
a{color:#06c}
code{background:#f4f4f4;padding:2px 6px;border-radius:3px}
.box{border:1px solid #ddd;border-radius:6px;padding:1rem;margin:1rem 0;background:#fafafa}
`

func homeHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	count := atomic.AddUint64(&visitCount, 1)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Hello from Go</title>
<link rel="stylesheet" href="style.css"></head>
<body><h1>Hello from a Go web app</h1>
<p>Served by a Go binary on Windows.</p>
<div class="box">Visit count (process lifetime): <strong>%d</strong></div>
<ul>
  <li><a href="time">/time</a> &mdash; current server time as JSON</li>
  <li><a href="echo?msg=hi">/echo?msg=hi</a> &mdash; echo a query parameter</li>
  <li><a href="style.css">/style.css</a> &mdash; this page's stylesheet</li>
</ul>
</body></html>`, count)
}

func styleHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/css; charset=utf-8")
	w.Header().Set("Cache-Control", "public, max-age=300")
	fmt.Fprint(w, styleCSS)
}

func timeHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"now":%q,"unix":%d}`+"\n",
		time.Now().UTC().Format(time.RFC3339), time.Now().Unix())
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
	msg := r.URL.Query().Get("msg")
	if msg == "" {
		msg = "(no msg query param)"
	}
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	fmt.Fprintln(w, msg)
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", homeHandler)
	mux.HandleFunc("/style.css", styleHandler)
	mux.HandleFunc("/time", timeHandler)
	mux.HandleFunc("/echo", echoHandler)

	srv := &http.Server{
		Addr:         listenAddr,
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
	}
	log.Printf("hello: listening on %s", listenAddr)
	log.Fatal(srv.ListenAndServe())
}

The Go standard library works identically on Windows — the same net/http, the same sync/atomic, the same JSON. The binary is a .exe but the source code is unchanged.

4. Build & Smoke-Test

In Command Prompt or PowerShell from the project directory:

go build -o hello.exe .
hello.exe
2026/05/22 hello: listening on :8081

Open http://localhost:8081 in your browser. You should see the HTML page. The visit counter increments on each load.

Or test with curl (available in Windows 10+) in a second terminal:

curl http://localhost:8081/time
{"now":"2026-05-22T13:00:00Z","unix":1779444000}

curl http://localhost:8081/echo?msg=works
works

Press Ctrl+C in the first terminal to stop it. The binary is statically compiled — no Go runtime to install on other Windows machines, just copy hello.exe and run it.

5. Run as a Windows Service with NSSM

A console app killed when you close the terminal is not useful for long-running servers. NSSM (Non-Sucking Service Manager) wraps any executable as a proper Windows Service that starts at boot and restarts on failure.

Download nssm.exe from nssm.cc/download and place it somewhere on your PATH (e.g. C:\Windows\System32\), then open an Administrator Command Prompt:

REM Install the service
nssm install GoHello "C:\Users\yourname\go-apps\hello\hello.exe"

REM Set the working directory
nssm set GoHello AppDirectory "C:\Users\yourname\go-apps\hello"

REM Log stdout and stderr to a file
nssm set GoHello AppStdout "C:\Users\yourname\go-apps\hello\hello.log"
nssm set GoHello AppStderr "C:\Users\yourname\go-apps\hello\hello.log"

REM Start the service now
nssm start GoHello

Replace yourname with your actual Windows username. You can confirm it is running in Services (services.msc) or with:

sc query GoHello
STATE : 4  RUNNING

The service will now start automatically on every boot. NSSM restarts the process if it exits unexpectedly.

To rebuild and restart after editing the code (Administrator prompt):

nssm stop GoHello
go build -o "C:\Users\yourname\go-apps\hello\hello.exe" "C:\Users\yourname\go-apps\hello"
nssm start GoHello

To remove the service entirely:

nssm remove GoHello confirm
NSSM also ships a GUI: just run nssm install without arguments and fill in the form. Useful the first time you set it up.

6. Open the Firewall

If you want other machines on your network (or the internet) to reach the app, Windows Firewall will block inbound traffic on port 8081 by default. Open an Administrator prompt and add a rule:

netsh advfirewall firewall add rule ^
  name="Go Hello App" ^
  dir=in action=allow protocol=TCP localport=8081

To remove the rule later:

netsh advfirewall firewall delete rule name="Go Hello App"
For a public-facing server you should also put nginx (available for Windows) or IIS in front as a reverse proxy, and bind the Go app to 127.0.0.1:8081 so it is not directly reachable from outside. For local development, direct browser access on :8081 is fine.

7. Useful Commands

REM Check service status
sc query GoHello

REM Stop / start the service (Administrator)
nssm stop GoHello
nssm start GoHello

REM Follow the log file live (PowerShell)
Get-Content C:\Users\yourname\go-apps\hello\hello.log -Wait

REM Find what process is using port 8081
netstat -ano | findstr :8081

REM Kill a process by PID (from the netstat output)
taskkill /PID 12345 /F