I’ve been coding for more than 10 years, and I’ve used a variety of languages, but when it comes to fill my pockets I code in Ruby.
I have been checking out Go for a while now. I like it’s simplicity while maintaining consistency across its methods and standard libraries. I have little experience with typed languages, but Go makes it feel easy, and the IDE tools it has makes it fun to use.
So what is this Tech Q I’m talking about? It’s a web application where I can randomly get questions for technical interviews I sometimes do at my work. I was looking to learn the following:
-
Embedding with
go:embed
1 : I’m blown away with the idea you can compile statics in a binary. I testedgo-bindata
andpackr
, but Go introduced a way in 2021. -
Serving paths with
http
: I’ve experimented withgorilla/mux
2 but wanted to use thehttp
3 module. -
Using
gorm
4 with SQLite5: I’ve been keeping an eye on this library for a while, wanted to give it a try using SQLite. - Organizing the app: I have experimented on how to modularize a Go application, but wanted to give it another go, to see what feels natural.
- Last but not least, to deploy something: It’s been a while and it’s always fun.
Let’s dive into what I learned for each of these points.
Embedding with go:embed
Turns out you can use the directive //go:embed
to access embedded files in a Go program. For this application I created the following structure.
resources
├── resources.go
├── static
│ ├── css
│ ├── fonts
│ └── images
└── templates
├── index.gohtml
└── layout.gohtml
Where resources.go
contains the following code:
package resources
import "embed"
var (
//go:embed templates
Templates embed.FS
//go:embed static
Statics embed.FS
)
This allows me to use the embedded files like this, for example the IndexHandler
:
func IndexHandler(w http.ResponseWriter, r *http.Request) {
// ...
tpl, err := template.ParseFS(
resources.Templates,
"templates/index.gohtml",
"templates/layout.gohtml",
)
// ...
}
For Statics
, which can be fonts, images, CSS, etc. my handler looks like this:
func StaticHandler() http.Handler {
var staticFS = http.FS(resources.Statics)
return http.FileServer(staticFS)
}
Finally, since my handlers are pointing to either /
in the case of the IndexHandler
and /statics/
for the StaticHandler
, I can access for example a CSS file from a layout with /statics/css/styles.css
.
While developing the application I even embedded a CSV file, the SQLite database itself, and images, just to try it out. It’s nice.
Serving paths with http
My server skills are still a mess, and I’m still copying and pasting some code I’ve used before. But I learned something this time, take a look at this code:
func NewServer(port string) *Server {
mux := http.NewServeMux()
httpServer := &http.Server{Addr: ":" + port, Handler: mux}
s := &Server{
httpServer: httpServer,
}
mux.HandleFunc("/", indexHandler)
return s
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
log.Println("indexHandler called")
}
Whenever I visited localhost:3000
I would get two log lines for the indexHandler
. I was confused so I added more information to the log line:
func IndexHandler(w http.ResponseWriter, r *http.Request) {
log.Println("indexHandler called", r.URL.Path)
}
Which logged:
indexHandler called /
indexHandler called /favicon.ico
That’s a request from me and the browser looking for /
and /favicon.ico
. The documentation about NewServerMux
says the following6:
Note that since a pattern ending in a slash names a rooted subtree, the pattern “/” matches all paths not matched by other registered patterns, not just the URL with Path == “/”.
Which makes sense, that’s the kind of flexibility I like. This small patch achieved what I was looking for to only match GET
at /
.
func IndexHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/" {
w.WriteHeader(http.StatusNotFound)
return
}
}
Using gorm
with SQLite
I’m used to Ruby on Rails7 and its wonderfully bloated ActiveRecord
and ActiveModel
. Don’t get me wrong, I love it, but when it comes to use other ORMs like gorm
, you feel there’s something missing, like migrations and seeds in my case.
I created a method to connect to the database:
var DB *gorm.DB
func ConnectDatabase() {
database, _ := gorm.Open(sqlite.Open("./data/database.db"), &gorm.Config{})
DB = database
}
And three methods to conviniently manage the database with my Questions
struct.
func DropDB(db *gorm.DB) {
db.Migrator().DropTable(&models.Question{})
}
func CreateDB(db *gorm.DB) {
db.AutoMigrate(&models.Question{})
}
func SetupDB(db *gorm.DB) {
DropDB(db)
CreateDB(db)
}
For the seeds I found this awesome article from Redha Juanda8, I adapted a method to read from a TXT file with the questions I needed.
func (s Seed) QuestionSeed() {
file, _ := os.Open("db/seeds/questions.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
s.db.Create(&models.Question{Body: line})
}
}
I used this code9 to handle arguments using flag
and adapted it to my needs.
func HandleArgs() {
flag.Parse()
args := flag.Args()
if len(args) >= 1 {
switch args[0] {
case "seed":
db.ExecuteSeed(db.DB, args[1:]...)
os.Exit(0)
case "create":
db.CreateDB(db.DB)
os.Exit(0)
case "drop":
db.DropDB(db.DB)
os.Exit(0)
case "setup":
db.SetupDB(db.DB)
db.ExecuteSeed(db.DB)
os.Exit(0)
}
}
}
Allowing me to execute them like this:
$ go run main.go create
$ go run main.go seed # or seed QuestionSeed
Organizing the app
I learned in my csgo_exporter
10 project how to organize a Prometheus Exporter, or at least what its usually done. For this case I took a similar approach but trying not to put everything in the internal
folder.
I kept config
because it makes sense. db
and seeds
in the same directory to maintain database logic in one place. resources
as I mentioned before it’s where the statics are. And internal
is everything that has to do with business logic.
.
├── config
├── data
├── db
│ └── seeds
├── internal
│ ├── handlers
│ ├── models
│ └── server
└── resources
├── static
│ ├── css
│ ├── fonts
│ └── images
└── templates
Now that I’m writing this maybe the server
package needs to be outside internal
? Or maybe inside config
? Trivialities, but I like this kind of stuff.
Deploy something
Feels good to deploy, no? I have an instance in DigitalOcean11 instance full of Docker containers with Traefik12 as a reverse proxy. The easiest way for me is to create an image and set it up with Docker Compose.
I really like this Dockerfile, it’s very similar to the one in the csgo_exporter
.
ARG IMAGE=scratch
ARG OS=linux
ARG ARCH=amd64
FROM golang:1.18.1-alpine3.15 as builder
WORKDIR /go/src/github.com/kinduff/techq
RUN apk --no-cache --virtual .build-deps add git alpine-sdk sqlite
COPY . .
RUN go mod download
RUN GOOS=$OS GOARCH=$ARCH go build -a -ldflags '-linkmode external -extldflags "-static"' -o binary .
FROM $IMAGE
LABEL name="techq"
WORKDIR /root/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/src/github.com/kinduff/techq/binary techq
EXPOSE 3000
CMD ["./techq"]
This generates a compressed image of ~8MB. The Docker Compose file is trivial.
You can see the final result at TechQ.kinduff.com ⧉ and the source code at github.com/kinduff/techq ⧉.
Hope you enjoyed this blog post, it was really fun to write and I’m always happy to share what I know and what I’m learning.
See you next time!
Footnotes