[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.
Also there is a function to retrieve the different templates depending on a string.
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
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.
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.
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:
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:
Also here are the functions of FileServer to handle different errors:
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.
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