Install Go with winget, build a web server, and run it as a persistent Windows Service using NSSM — no admin scripting required.
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.
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:
/ — HTML home page with a live visit counter/style.css — the stylesheet served by the Go binary itself/time — current server time as JSON/echo?msg=hi — echoes a query parameter as plain textIt listens on :8081 so you can open it in your browser at http://localhost:8081 immediately, no reverse proxy needed.
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> — current server time as JSON</li>
<li><a href="echo?msg=hi">/echo?msg=hi</a> — echo a query parameter</li>
<li><a href="style.css">/style.css</a> — 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.
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.
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 install without arguments and fill in the form. Useful the first time you set it up.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"
127.0.0.1:8081 so it is not directly reachable from outside. For local development, direct browser access on :8081 is fine.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