| title | description |
|---|---|
Development Guide |
How to build, test, lint, and contribute to go-ws. |
This document covers everything needed to work on go-ws: prerequisites, building, testing, coding standards, and contribution workflow.
- Go 1.26 or later. Benchmarks use
b.Loop()which requires Go 1.25+; thego.moddeclares 1.26. - Redis (optional). The
RedisBridgeintegration tests connect to a Redis instance at10.69.69.87:6379. When Redis is unreachable, these tests are automatically skipped. You do not need Redis for general development. - No CGO. The module has no C dependencies and builds on Linux, macOS, and Windows without additional toolchains.
go build ./...There is no Taskfile or Makefile. All automation goes through the standard go toolchain or via core go commands if you have the Core CLI installed.
# Full test suite
go test ./...
# With race detector (required before every commit)
go test -race ./...
# Single test by name
go test -v -run TestHub_Run ./...
# Benchmarks
go test -bench=. -benchmem ./...
# Specific benchmark
go test -bench=BenchmarkBroadcast_100 -benchmem ./...Tests live in the same package (package ws), giving white-box access to unexported fields. Four test files exist:
| File | What it covers |
|---|---|
ws_test.go |
Hub lifecycle, broadcast, channel send, subscribe/unsubscribe, readPump, writePump, integration via httptest, auth integration, reconnecting client |
auth_test.go |
APIKeyAuthenticator, BearerTokenAuth, QueryTokenAuth, AuthenticatorFunc, nil authenticator, OnAuthFailure callback |
redis_test.go |
RedisBridge creation, lifecycle, broadcast, channel delivery, cross-bridge messaging, loop prevention, concurrent publishes, graceful shutdown, context cancellation |
ws_bench_test.go |
9 benchmarks: broadcast (100 clients), channel send (50 subscribers), parallel variants, JSON marshal, WebSocket end-to-end round-trip, subscribe/unsubscribe cycle, multi-channel fanout, concurrent subscribers |
Redis integration tests call skipIfNoRedis(t) at the top, which pings the configured address and calls t.Skip if unreachable:
func TestRedisBridge_PublishBroadcast(t *testing.T) {
client := skipIfNoRedis(t)
// ...
}Each Redis test uses a unique time-based prefix (testPrefix(t)) to avoid key collisions between parallel runs. Cleanup runs via t.Cleanup.
Integration tests use httptest.NewServer with hub.Handler() and connect a real gorilla/websocket client:
server := httptest.NewServer(hub.Handler())
defer server.Close()
url := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(url, nil)This exercises the full upgrade handshake, readPump, writePump, and message serialisation.
Benchmarks use b.Loop() (Go 1.25+) and b.ReportAllocs():
func BenchmarkBroadcast_100(b *testing.B) {
// setup ...
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
_ = hub.Broadcast(msg)
}
b.StopTimer()
// drain ...
}The project uses golangci-lint with the configuration in .golangci.yml. Enabled linters include govet, errcheck, staticcheck, unused, gosimple, ineffassign, typecheck, gocritic, and gofmt.
# Lint
golangci-lint run ./...
# Or via Core CLI
core go lint
# Vet (also run by golangci-lint, but useful standalone)
go vet ./...Use UK English throughout: "initialise", "colour", "behaviour", "cancelled", "unauthorised", "organisation", "centre". Never American spellings.
Every .go source file (including tests) must begin with:
// SPDX-Licence-Identifier: EUPL-1.2- All exported symbols must have doc comments.
- Error strings are lowercase and do not end with punctuation.
- Use named return values only when they materially clarify intent.
- Prefer
require.NoErroroverassert.NoErrorwhen a test cannot continue after failure. - Standard
gofmtformatting. No additional formatter configuration.
Go files use tabs (as per gofmt). Markdown, YAML, and JSON files use 2-space indentation (configured in .editorconfig).
Commits follow the Conventional Commits specification:
type(scope): description
Common types: feat, fix, test, docs, refactor, bench.
Scope is the logical area: ws, auth, redis, reconnect.
Every commit includes the co-author trailer:
Co-Authored-By: Virgil <[email protected]>
Example:
feat(redis): add envelope pattern for loop prevention
Generated a random sourceID per bridge instance at construction.
Listener drops messages where sourceID matches local bridge.
Co-Authored-By: Virgil <[email protected]>
- Add a
MessageTypeconstant to the block inws.go. - Handle the new type in
readPumpif it is client-initiated. - Add a helper method on
Hubif it is server-initiated (following the pattern ofSendProcessOutput). - Write tests in
ws_test.go. - Update the message type table in
docs/architecture.md.
Implement the Authenticator interface:
type MyJWTAuth struct { /* ... */ }
func (a *MyJWTAuth) Authenticate(r *http.Request) ws.AuthResult {
token := r.Header.Get("Authorization")
claims, err := validateJWT(token)
if err != nil {
return ws.AuthResult{Valid: false, Error: err}
}
return ws.AuthResult{
Valid: true,
UserID: claims.Subject,
Claims: map[string]any{"roles": claims.Roles},
}
}Pass it to HubConfig.Authenticator. The existing APIKeyAuthenticator, BearerTokenAuth, and QueryTokenAuth in auth.go serve as reference implementations. JWT libraries are intentionally not imported; consumers bring their own.
If you have the Core CLI installed:
core go qa # fmt + vet + lint + test
core go qa full # + race, vuln, securityOtherwise, run each step manually:
gofmt -l .
go vet ./...
golangci-lint run ./...
go test -race ./...| Forge | ssh://[email protected]:2223/core/go-ws.git |
| Push | SSH only; HTTPS authentication is not configured on Forge |
| Licence | EUPL-1.2 |