|
3 months ago | |
---|---|---|
cmd/ratchetd | 3 months ago | |
internal | 3 months ago | |
migrations | 3 months ago | |
ui | 3 months ago | |
.air.toml | 3 months ago | |
.gitignore | 3 months ago | |
LICENSE | 3 months ago | |
Makefile | 3 months ago | |
README.md | 3 months ago | |
go.mod | 3 months ago | |
go.sum | 3 months ago | |
ratchet.go | 3 months ago |
README.md
Ratchet
An example web application in Golang.
https://lets-go.alexedwards.net/sample/02.09-serving-static-files.html
Project Structure
Following https://go.dev/doc/modules/layout#server-project the implementation
Loosely inspired by the organization of WTFDial,
- Application domain types reside in the project root (User, UserService, etc)
- Implementations of the application domain reside in the subpackages
sqlite
,http
, etc. - Everything is tied together in the
cmd
subpackages
Application Domain
This is project is an Indie reader inspired by Feedi.
Implementation Subpackages
The subpackages function as an adapter between our domain and the technology used to implement the domain. For example sqlite.FeedService
implements the ratchet.FeedService
using SQLite.
Subpackages ideally SHOULD NOT know about one another and SHOULD communicate in terms of the application domain.
A special mock
packages can be used to create simple mocks for each application domain interface. This will allow each subpackages unit tests to share a common set of mocks so layers can be tested in isolatation.
Binary Packages
With loosely coupledc subpackages the application is wired together in the cmd
subpackages to produce a final binary.
The cmd
packages are ultimately the interface between the application domain and the operator. Configuration of types and any CLI flags SHOULD live in these packages.
Development
You can build ratchet
locally by cloning the respository, then run
make
go install ./cmd/...
The ratchetd
cmd binary uses Oauth so you will need to create a new Oauth App. The vlaue of the authorization callback must be the hostname and IP at which clients can access the ratchetd
server.
Additional Resources
- Content Range Requests
- HTTP 204 and 205 Status Codes
- How to Disable FileServer Directory Listings
- Understand Mutexs in Go
- Structured Logging in Go with log/slog
- Exit Codes with special meaning
- Organizing Database Access in Go
Go http.FileServer
Supports If-Modified-Since and Last-Modified headers
curl -i -H "If-Modified-Since:
Thu, 04 May 2017 13:07:52 GMT" http://localhost:5001/static/img/logo.png
HTTP/1.1 304 Not Modified
Last-Modified: Thu, 04 May 2017 13:07:52 GMT
Date: Sun, 12 Jan 2025 14:26:06 GMT
Supports Range Requests and 206 Partial Content responses.
curl -i -H "Range: bytes=100-199" --output - http://localhost:5001/static/img/logo.png
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 100
Content-Range: bytes 100-199/1075
Content-Type: image/png
Last-Modified: Thu, 04 May 2017 13:07:52 GMT
Date: Sun, 12 Jan 2025 14:18:32 GMT
-
The
Content-Type
is automatically set from the file extension using themime.TypeByExtension()
function. You can add your own custom extensions and content types using themime.AddExtensionType()
function if necessary. -
Sometimes you might want to serve a single file from within a handler. For this there’s the
http.ServeFile()
function, which you can use like so:
func downloadHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./ui/static/file.zip")
}
Warning: http.ServeFile() does not automatically sanitize the file path. If you’re constructing a file path from untrusted user input, to avoid directory traversal attacks you must sanitize the input with filepath.Clean() before using it.
Databases
The Let's-go book calls for MySQL. We use go-sqlite3 and go-migrate tool to manage migrations instead. Use make check-system-deps
to validate all tools for this repository are installed.
Packages to consider adopting include sqlx or https://github.com/blockloop/scan. Both have the potential to reduce the verbosity that comes will using the standard database/sql package.
Nulls Gotcha
Beware of Nulls when using DB.Scan()
To avoid issues consider using the sql.Null* types or putting a NOT NULL
constraint in your DDL. Also a sensible Default
Managing Dependencies
To upgrade to latest minor/patch version of a package in your go mod.go you can use the -u
flag:
go get -u github.com/foo/bar
To update to a specific package
go get -u github.com/foo/bar@v2.0.0
To remove the package you can use go mod tidy
if all references have been removed, or:
go get github.com/foo/bar@none
Templates
The official template docs can be confusing to sort out. If you are new to templates check this out first: https://www.evandemond.com/kb/go-templates
- Always use
html/templates
because it escapes characters, preventing XSS attacks. - With Nested templates you must explicitly pass or pipeline your template data. This occurs in
{{template}}
or{{block}}
and appears like{{template "main" .}}
or{{block "sidebar" .}}{{end}}
- If the type that you’re yielding between
{{ }}
tags has methods defined against it, you can call these methods (so long as they are exported and they return only a single value — or a single value and an error). Example.Snippet.Created
struct field has the underlying typetime.Time
so<span>{{.Snippet.Created.Weekday}}</span>
. You can also pass parameters<span>{{.Snippet.Created.AddDate 0 6 0}}</span>
- The
html/template
package always strips out any HTML comments you include in your templates which also help avoid XSS attacks when rendering dynamic content. - Variable names must be prefixed by a dollar sign and can contain alphanumeric characters only. Ex:
{{$bar := len .Foo}}
- You can combine multiple template functions with
()
. Ex:{{if (gt (len .Foo) 99)}} C1 {{end}}
- Within a
{{range}}
action you can use the{{break}}
command to end the loop early, and{{continue}}
to immediately start the next loop iteration.
Template Actions
Action | Description |
---|---|
{{ define }} |
Defines a reusable named template. The content inside {{ define "name" }} ... {{ end }} can be invoked using {{ template "name" }} . Nested define calls are not allowed. |
{{ template }} |
Executes a template defined by {{ define "name" }} . Can optionally pass data into the template as dot (e.g., {{ template "name" .Data }} ). |
{{ block }} |
Defines a named template block, similar to define , but allows the content to be overridden when embedding templates. Often used in base templates to create extendable sections. |
{{/* a comment */}} |
A comment that is ignored by the template engine. Use it to add notes or disable parts of the template without affecting the rendered output. |
Template Functions
- For all actions below the
{{else}}
clause is optional. - The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.
- the
with
andrange
actions change the value of dot. Once you start using them, what dot represents can be different depending on where you are in the template and what you’re doing. -}}
and{{-
trims white space."{{23 -}} < {{- 45}}"
becomes"23<45"
Function | Description |
---|---|
{{ if .Foo }} C1 {{ else }} C2 {{ end }} |
If .Foo is not empty then render the content C1, otherwise render the content C2. |
{{ with .Foo }} C1 {{ else }} C2 {{ end }} |
If .Foo is not empty, then set dot to the value of .Foo and render the content C1, otherwise render the content C2. |
{{ range .Foo }} C1 {{ else }} C2 {{ end }} |
If the length of .Foo is greater than zero then loop over each element, setting dot to the value of each element and rendering the content C1. If the length of .Foo is zero then render the content C2. The underlying type of .Foo must be an array , slice , map , or channel . |
{{ define }} |
|
{{ define }} |
Action | Description |
---|---|
{{ eq .Foo .Bar }} |
Yields true if .Foo is equal to .Bar . |
{{ ne .Foo .Bar }} |
Yields true if .Foo is not equal to .Bar . |
{{ not .Foo }} |
Yields the boolean negation of .Foo . |
{{ or .Foo .Bar }} |
Yields .Foo if .Foo is not empty; otherwise yields .Bar . |
{{ index .Foo i }} |
Yields the value of .Foo at index i . The underlying type of .Foo must be a map, slice, or array, and i must be an integer value. |
{{ printf "%s-%s" .Foo .Bar }} |
Yields a formatted string containing the .Foo and .Bar values. Works in the same way as fmt.Sprintf() . |
{{ len .Foo }} |
Yields the length of .Foo as an integer. |
{{$bar := len .Foo}} |
Assigns the length of .Foo to the template variable $bar . |
Middleware
- Function that forms a closure over the
next.ServerHTTP
function in a call chain myMiddleware → servemux → application handler
applies to all requestsservemux → myMiddleware → application handler
wraps specific handlers. Example Auth middleware.- The control flow actually looks like
commonHeaders → servemux → application handler → servemux → commonHeaders
- This means defered blocks or code after
next.ServeHTTP()
will execute on the way back through.
- This means defered blocks or code after
- Early returns before
next.ServeHTTP()
will hand controlflow back upsteam.- Auth middle ware is a good example.
w.WriteHeader(http.StatusForbidden); return
- Auth middle ware is a good example.