Let's Go
Creating a module
- Create a directort for your project
mkdir snippetbox
. -
Convention is to use your URL as the module path i.e.
${project_name}.{your_domain}
, for examplesnippetbox.kushajveersingh.com
.If the project is supposed to be downloaded by other people, then the location from where the project can be downloaded should be the module path, for example
github.com/foo/bar
. -
Run
go mod init snippetbox.kushajveersingh.com
in the directory to convert it into a module.It will create
go.mod
file with the following contents
Hello world example
-
Create
main.go
with the content -
Run it using
go run .
.This command compiles your code, creates an executable binary in
/tmp
, and then runs the binary. It can accept directory path, go file path, module path. So the next three commands are equivalent -
To format the code run
go fmt
and it will format all the files in the directory.
Web application basics
We need three components
- Handler - similar to controllers in MVC. It's job is to execute application logic and write HTTP reponse headers and bodies.
-
Router/servemux - It stores a mapping between the URL patterns for your application adn the corresponding handlers.
Note:- Go also provides a default router called
http.DefaultServeMux
. Do not use it, as it is less explicit.Two type of URL patterns are supported
- Fixed paths (do not end with
/
, like/snippet/create
) - the URL should exactly match for the handler function to be invoked. - Subtree paths (that end with
/
, like/
, or/static/
) - if the start of the URL matches the pattern, then the handler function is invoked.
In case multiple patterns match the URL, then the longer pattern takes percedence.
Request URLs are also sanitized to handle
.
,..
, remove extra/
. For example,/foo/bar/..//baz
sends a301 Permanent Redirect
to/foo/baz
.If a subtree path has been registred (say
/foo/
), then the request for/foo
, would be redirected with301 Permanent Redirect
to/foo/
.You can further redirect host names also, and this is why you don't need Nginx, Apace
- In this case, when a request is received, the host name routes would be checked first.
-
If no match is found, then only non-host name routes would be checked.
- Fixed paths (do not end with
-
Web server - In Go, this is part of the application itself, and you don't need an external third-party server like Nginx, Apache.
Internal details
- All incoming HTTP requests are handled concurrently (in parallel), and this means you need to be aware of (and protect against) race conditions when accessing shared resources from your handlers.
-
mux.HandleFunc
is a shorthand for converting any function to a handler. This works by adding aServeHTTP()
method to the function. So below two pieces of code are equivalent
Restrict the root url pattern
If you do not want "/"
to act as a catch-all, and display a 404 page not found
response for all invalid routes, modify the home handler. You cannot modify the behavior of servemux.
Customize HTTP headers
/snippet/create
should only respond to HTTP requests with POST
method. Also, we will add the info that only POST requests are allowed in the HTTP response.
Now you can test it using curl -i -X POST http://localhost:4000/snippet/create
, and the response is
Or if you try a GET
request the response it
In practice, you rarely use w.Write
, w.WriteHeader
directly. Instead, you use http.Error
for the above example. Also, use http.MethodPost
instead of the string POST
and http.StatusMethodNotAllowed
instead of the integer constant 405
(these help avoid runtime error, and act as self documentation).
By default, Go sets three system-generated headers with every response
- Date
- Content-Length
- Content-Type
- Go uses http.DetectContentType()
function for this and uses application/octet-stream
as the fallback. This function cannot distinguish text from json. So for JSON response you have to set it manually using !#go w.Header().Set("Content-Type", "application/json")
.
To remove the system-generated headers, set their values to nil
directly in the header map
Modify the HTTP header map using these functions
Cache-Control
in the above example is case-insenstiive, as Go uses textproto.CanonicalMIMEHeaderKet()
to convert the header string to the correct format. For HTTP/2, it converts everything to lowercase. If you do not want this behavior, then you can edit the header map directly as
Project structure
https://github.com/thockin/go-build-template is a good reference.
For our simple project we can use this structure
-
cmd
- contain application-specific code for the executable applications in the project. For now, we only have one executable application (the web application) atcmd/web
.If in the future you want to add an CLI application to automate some admin functions you can create that at
cmd/cli
. -
internal
- non-application-specific code (like utils). This can include validation helpers, SQL database models.internal
directory also has a special behavior. Any code inside this directory, can be imported by code inside the parent of theinternal
directory i.e. packages insideinternal
can only be imported by code insidesnippetbox
directory. So now people cannot use your GitHub code directly and have to import it from a package. -
ui
- user interface assets used by the web application.
Create HTML templates
If you are just using a single HTML file without a template use these steps.
Create ui/html/pages/home.tmpl.html
file with all your HTML content. Here .tmpl
is just used to signify that it is a template file (it does nothing special).
Then invoke if from the required handler
If you are defining tempalates, then use these steps.
For HTML, you can define base templates and reuse them across pages
{{define "base"}}
to mark start of a named template, and{{end}}
to mark the end.{{tempalte "title" .}}
,{{template "main" .}}
- to invoke named templates called 'title' and 'main'. The dot here represent any dynamic data you want to pass to the template.- Define partials (reusable bits of HTML for different pages) in
ui/html/partials
. -
Tempaltes can further define blocks, that be used to modify the code inside template. So the template can define a default sidebar, and then pages can modify it if needed.
Define a partial for navigation in ui/html/partial/nav.tmpl.html
Define the base template in ui/html/base.tmpl.html
(and invoke the partial just like a normal template)
Define the home template now ui/html/pages/home.tmpl.html
(define the named templates, that you want to use inside the base template)
Update the handler code to parse both template files
Handle static files
To host static files like images/css/js, create a file server using http.FileServer
, and you need to create a route /static/
for that.
Add the handler to the router
Now you can use static assets in your HTML by refering the path /static/
. For example
Also, you can go to localhost:4000/static
to see the FTP server.
Extra features of http.FileServer
- Sanitizes request path using path.Clean()
before searching for a file to remove any .
, ..
from the url path, which helps to stop directory traversal attacks.
- "Range requests" are supported to allow resuamble downloads. For example, you can request bytes 100-199 of the logo.png
curl -i -H "Range: bytes=100-199" --output - http://localhost:4000/static/img/logo.png
.
- Supports Last-Modified
and If-Modified-Since
headers. This means if the file hasen't changed since the user last requested it, then 304 Not Modified
would be sent.
- Content-Type
is automatically set using the file extension. And to add custom extensions you can use mime.AddExtensionType()
.
Serve files from a handler
To serve a specific file in response to a request. This is useful for - Dynamic File Serving - determine the file to serve based on some logic - Access Control - serve the file only if the user is authorized - Logging and monitoring - Modification of reponse headers before sending the file.
Disable directory listing
Solution 1.
- If you naviagte to /static
, then all the files inside the directory would be listed. But you can prevent this by adding an empty index.html
files inside static
folder, and now this would be shown instead of directory listing.
- Now if you navigate to /static/js
, then it would still list all the files in that directory.
- So you have to add index.html
to all the directories and that can be done using find ./ui/static -type d -exec touch {}/index.html \;
.
- This solution would send 200 OK
response.
A better solution is to create a custom FileServer
continue from 03.01-managing-configuration-settings.html
Created: November 21, 2023