[goshs] Part #2 - Trying to achieve code quality
Tutorial Series
This is Part 2 of a tutorial series. Also see:
- Part#1 - My take on SimpleHTTPServer in go
- Part#2 - Trying to achieve code quality
- Part#3 - I can haz featurez?
- Part#4 - Eyecandy, anyone?
Summary
In this chapter I want to split up the code a bit. So my goal is to reduce the main function
to its minimum and to outsource different parts of the application to different topic related files. The separate files I chose will be templates
, fileserver
and log
.
So to illustrate what my project folder looks like after finishing this post, look at my tree output:
❯ tree -L 3
.
├── build
│ └── goshs
├── go.mod
├── internal
│ ├── myhtml
│ │ └── template.go
│ ├── myhttp
│ │ └── fileserver.go
│ └── mylog
│ └── log.go
├── main.go
├── Makefile
└── README.md
And also please notice that I chose to continue by using go modules
and a git repository. I will not handle what a go module is. If you need to read up the topic look at the official go blog post on that topic.
Makefile
I am a big fan of having a Makefile around. In this case it is not a big one:
.PHONY: build
build:
@go build -o build
@echo "[OK] App binary was created!"
run:
@./build/goshs
All it does is to handle build and run by executing the go equivalents. I guess this is really self explanatory.
The Templates
To be more flexible with the templates to deliver to the client I made a package called myhtml under the path internal/myhtml/template.go
.
It basically consist of three templates. I added a template to handle file permission issues as you will see further down the lines.
const dispTmpl = `
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Directory listing for {{.Path}}</title>
</head>
<body>
<h1>Directory listing for {{.Path}}</h1>
<hr />
<ul>
{{range .Content}}
<li><a href="/{{.URI}}">{{.Name}}</a></li>
{{ end }}
</ul>
<hr />
</body>
</html>
`
const notFoundTmpl = `
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: File not found.</p>
<p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>
</body>
</html>
`
const noAccessTmpl = `
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 500</p>
<p>Message: No permission to access the file.</p>
<p>Error code explanation: HTTPStatus.PERMISSION_DENIED - You have no permission to access the file.</p>
</body>
</html>
`ternal/myhtml/template.go" >}}
Also there is a function to retrieve the different templates depending on a string.
// GetTemplate will deliver the template depending on a 'name'
func GetTemplate(name string) string {
switch name {
case "display":
return dispTmpl
case "404":
return notFoundTmpl
case "500":
return noAccessTmpl
}
return ""
}
So this is the template.go
. It will be used in the fileserver.go
when handling the http requests.
The Logging
I wanted to have a better looking logging and decided to use the log
standard lib to handle the logging for me. I chose to handle the logging in a separate file in a package called mylog located at internal/mylog/log.go
package mylog
import (
"log"
)
//LogRequest will log the request in a uniform way
func LogRequest(remoteAddr, method, url, proto, status string) {
if status == "500" || status == "404" {
log.Printf("ERROR: %s - - \"%s %s %s\" - %+v", remoteAddr, method, url, proto, status)
return
}
log.Printf("INFO: %s - - \"%s %s %s\" - %+v", remoteAddr, method, url, proto, status)
}
//LogMessage will log an arbitrary message to the console
func LogMessage(message string) {
log.Println(message)
}
There are two main functions. They are almost the same as in the initial run of this project. So the function LogRequest
will recieve the parameters needed to log a message, like remote address
or url
. It will then print the log line. Also the function LogMessage
can log arbitrary messages. As you might see we are missing the timestamp creation. This will be handled by the library log
by default.
The resulting output will look like this:
2020/10/06 14:22:07 Serving HTTP on 0.0.0.0 port 8000 from /tmp
2020/10/06 14:22:10 INFO: [::1]:52878 - - "GET / HTTP/1.1" - 200
2020/10/06 14:22:24 INFO: [::1]:52878 - - "GET /.battery HTTP/1.1" - 200
2020/10/06 14:22:26 ERROR: [::1]:52878 - - "GET /foo.txt HTTP/1.1" - 404
2020/10/06 14:22:26 404: File not found
The File Server
Well, this one is the biggest change. I wanted to have a very clear defined custom FileServer
we are using to handle everything related to http.
First of all I moved my previously defined structs from main.go
to the new package called myhttp located at internal/myhttp/fileserver.go
. Also I defined a new struct to hold the FileServers information. As I included a little new feature I not only defined the port as a field, but the webroot as well. So later on we are able to serve content from another directory than the current working dir.
type directory struct {
Path string
Content []item
}
type item struct {
URI string
Name string
}
// FileServer holds the fileserver information
type FileServer struct {
Port int
Webroot string
}
Next up I extend the struct FileServer to have three functions: router
, Start
and ServeHTTP
. Capitalized functions will be exposed to other packages and can be called. So router will only be internal, but Start and ServerHTTP will be accessible from our main function.
// router will hook up the webroot with our fileserver
func (fs *FileServer) router() {
http.Handle("/", fs)
}
// Start will start the file server
func (fs *FileServer) Start() {
// init router
fs.router()
// Print to console
log.Printf("Serving HTTP on 0.0.0.0 port %+v from %+v\n", fs.Port, fs.Webroot)
add := fmt.Sprintf(":%+v", fs.Port)
log.Panic(http.ListenAndServe(add, nil))
}
// ServeHTTP will serve the response by leveraging our handler
func (fs *FileServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, fmt.Sprintf("%+v", err), http.StatusInternalServerError)
}
}()
fs.handler(w, req)
}
As you can see the router function just tells our http server to handle every request within the file server itself. The function http.Handle
expects a http.Handler which our file server automatically provides with the function ServerHTTP. And the Start function starts the http server with the provided port and webroot. The ServeHTTP function will call our FileServers handler after handling possible errors.
Next up the heart of our code is the FileServers handler:
// handler is the function which actually handles dir or file retrieval
func (fs *FileServer) handler(w http.ResponseWriter, req *http.Request) {
// Get url so you can extract Headline and title
upath := req.URL.Path
// Ignore default browser call to /favicon.ico
if upath == "/favicon.ico" {
return
}
// Define absolute path
open := fs.Webroot + path.Clean(upath)
// Check if you are in a dir
file, err := os.Open(open)
if os.IsNotExist(err) {
// Handle as 404
fs.handle404(w, req)
return
}
if os.IsPermission(err) {
// Handle as 500
fs.handle500(w, req)
return
}
if err != nil {
// Handle general error
log.Println(err)
return
}
defer file.Close()
// Log request
mylog.LogRequest(req.RemoteAddr, req.Method, req.URL.Path, req.Proto, "200")
// Switch and check if dir
stat, _ := file.Stat()
if stat.IsDir() {
fs.processDir(w, req, file, upath)
} else {
fs.sendFile(w, file)
}
}
Basically not much changed. I decided to directly open the file/directory given instead of doing a stat first. Also I extended the error checking when accessing the file/directory by handling permission denied issues.
Then I use the new mylog package to do the logging. Afterwards I switch over the result of IsDir()
and call one of the two new FileServer functions processDir
or sendFile
.
Those are:
func (fs *FileServer) processDir(w http.ResponseWriter, req *http.Request, file *os.File, relpath string) {
// Read directory FileInfo
fis, err := file.Readdir(-1)
if err != nil {
fs.handle404(w, req)
return
}
// Create empty slice
items := make([]item, 0, len(fis))
// Iterate over FileInfo of dir
for _, fi := range fis {
// Set name and uri
itemname := fi.Name()
itemuri := url.PathEscape(path.Join(relpath, itemname))
// Add / to name if dir
if fi.IsDir() {
itemname += "/"
}
// define item struct
item := item{
Name: itemname,
URI: itemuri,
}
// Add to items slice
items = append(items, item)
}
// Sort slice all lowercase
sort.Slice(items, func(i, j int) bool {
return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
})
// Template parsing and writing to browser
t := template.New("index")
t.Parse(myhtml.GetTemplate("display"))
d := &directory{Path: relpath, Content: items}
t.Execute(w, d)
}
func (fs *FileServer) sendFile(w http.ResponseWriter, file *os.File) {
// Write to browser
io.Copy(w, file)
}
Also here are the functions of FileServer to handle different errors:
func (fs *FileServer) handle404(w http.ResponseWriter, req *http.Request) {
mylog.LogRequest(req.RemoteAddr, req.Method, req.URL.Path, req.Proto, "404")
mylog.LogMessage("404: File not found")
t := template.New("404")
t.Parse(myhtml.GetTemplate("404"))
t.Execute(w, nil)
}
func (fs *FileServer) handle500(w http.ResponseWriter, req *http.Request) {
mylog.LogRequest(req.RemoteAddr, req.Method, req.URL.Path, req.Proto, "500")
mylog.LogMessage("500: No permission to access the file")
t := template.New("500")
t.Parse(myhtml.GetTemplate("500"))
t.Execute(w, nil)
}
With everything setup like this the code is much more readable and understandable and more flexible for later modifications. You can implement new templates by extending the template.go
for example. Or if you decide to implement an upload function you extend the fileserver.go
file and start like this:
func (fs *FileServer) upload(w http.ResponseWriter, req *http.Request) {
}
The main.go
So to use our new SimpleHTTPServer
I had to change the main.go, as well. And what is left in here is minimal.
var (
port = 8000
webroot = "."
)
func init() {
wd, _ := os.Getwd()
// flags
flag.IntVar(&port, "p", port, "The port")
flag.StringVar(&webroot, "d", wd, "Web root directory")
flag.Parse()
}
func main() {
server := &myhttp.FileServer{Port: port, Webroot: webroot}
server.Start()
}
First the defaults are defined. Then I use the standard library flag
within the init()
function to handle the arguments one can hand to our application. Also It produces a help page automatically:
❯ ./build/goshs -h
Usage of ./build/goshs:
-d string
Web root directory (default "/tmp")
-p int
The port (default 8000)
The init()
function in main.go is called once before executing the main function. So there is no need to call it seperately.
Finally in the main()
function we initiate our custom myhttp.FileServer and start it.
That is all there is for todays tutorial. If you have questions do not hestitate to ask in the comment section below. Also check out the full code at github.com/patrickhener/goshs - tag v.0.0.2.
Thanks for taking the time to read through and have a nice day.
Patrick