Attempting to Learn Go - Building Dev Log Part 01

Building Out A Dev Blog

I’ve wanted to start cross-posting my dev.to posts over to my own site for a little while now but, had not quite decided how I wanted to do it. There are a couple different options - I can stand up a new Ghost instance or I can go with a static site generator. I love Ghost! I’m sad to say I haven’t worked on or with it much recently though. Using it for my “dev site” seems somehow wrong though - especially when I’m thinking that the main focus of the site will be Go. I’ve just been enjoying messing around with it that much. Gophercon or bust! (Probably bust because I’m not sure I can afford to “go”, har har.)


To Hugo Or Not Hugo

If you happen to have any visibility into the world of “static site generators” you may have heard of Hugo. Hugo is one of the most popular generators out there and it just happens to be written in Go. I have played with Hugo a little bit and even contributed one pull request to the documentation site but, the little I touched I liked very much. However, I think using Hugo would be against the spirit and purpose of these posts. We’re trying to write code to help us learn more and more Go so with that in mind let’s…


Not Hugo

Alright, so we’ve decided we won’t use Hugo for our site. So, what are we going to do then? I’ve been a bit undecided on that point so I’m just going to try something and we’ll see how it goes. I think the first iteration of the dev site will stick to being a static site, to that end we’ll be setting up everything behind Nginx and converting our existing markdown files to HTML using our own static site generator. Later we may switch over to hosting with a Go program so we can look at building out web servers with Go.


What Could Go Wrong

Let’s break this down. We’ll start with one test markdown file. First, we need to open it. That is very easy using ioutils.ReadFile(), so we’ll build out a small loadFile() function. It will take in a string, in this case, it will be the path to a file, and return a slice of bytes or an error. That should cover the first potential issue, that the file can’t be read for some reason.

Then we need to split into two sections - the front matter and the markdown. Before that though, what can we do to make sure the file is “valid”? Let’s look at the expected format of the file.

---
title: "test file"
published: false
description: "This is the description from the test file"
tags: these, are, tags
cover_image: https://someimage
series: "Attempting to Learn Go"
---
Post body goes here.

So we’ve got an opening line with --- followed by some YAML and then a closing --- to round out the front matter. We want a quick test to make sure the file has a valid front matter section and after thinking about it for a moment I think the fastest would be to just use bytes.Split() passing in our byte slice and the front matter delimiter, ---. Splitting should give us a slice of slices which is a valid file should have at least a length of 3. The first (b[0]) should be empty, the second (b[1]) will contain the []byte that makes up our front matter, and any remaining bits should be the rest of our post body.


Lets Get Coding

As always we’ll declare our package and our imports. We’re using some pretty basic stuff from the Go standard library along with an import for dealing with the YAML inside the front matter header. The Go standard library doesn’t have YAML support so I basically just Googled, “golang yaml” and clicked on the first GitHub link.

package main

import (
  "bytes"
  "fmt"
  "io/ioutil"
  "strings"

  yaml "gopkg.in/yaml.v2"
)

Imports out of the way let’s create our delimiter constant. We also know the basic makeup of a front matter header so, let’s create a struct for that now.

const delim = "---"

type post struct {
  title       string
  published   bool
  description string
  tags        []string
  coverImage  string
  series      string
}

For this step we’re only going to have one function outside of main() and that will be loadFile() func. We’ll likely extend this out over the next few revisions, which is really the only reason it isn’t just stuck in main(), but for now we’ll keep it short and sweet. It’s pretty self-explanatory as the standard library does all the heavy lifting for us.

func loadFile(s string) (b []byte, err error) {
  f, err := ioutil.ReadFile(s)
  if err != nil {
    return nil, err
  }
  return f, nil
}

Crusin Down Main

We’ll start off by calling loadFile() to load our test file into memory.

func main() {
  f, err := loadFile("test.md")
  if err != nil {
    panic(err)
  }

Now we’ll try the quick and dirty method to validate the file.

  b := bytes.Split(f, []byte(delim))
  if len(b) < 3 || len(b[0]) != 0 {
    panic(fmt.Errorf("Front matter is damaged"))
  }

The first thing you see below is we’re making a new map, map[string]interface{}. This is to hold the front matter YAML data pulled off the top of the file. I will not do explaining interfaces any justice - so, I recommend reading Jordan Orelli’s How to use interfaces in Go post which does an excellent job. We’re going to feed our b[1] directly into it. The nice thing about doing it this way is we know that improperly formatted YAML will throw an error. I’m sure there are some edge cases but, I’m confident enough for this version of the code that it will be OK. We’ll simply use yaml.Unmarshal() to create our map. So the post object might look something like this, &post{title:"test file", ...}.

  m := make(map[string]interface{})
  err = yaml.Unmarshal([]byte(b[1]), &m)
  if err != nil {
    msg := fmt.Sprintf("error: %v\ninput:\n%s", err, b[1])
    panic(msg)
  }

We now create empty struct p which we’ll use to hold our post data. Later on, we’ll use this post data to file in our HTML templates. We can’t just use m["title"] and assign it into our struct as everything currently has a type of interface{} so we’ll assert the type using .(type). You could also use something like fmt.Sprintf("%v", m["title"]) to cast the interface to the proper type, string in this example. We take an extra step with the tags to make sure we end up with a slice and not a string of comma separated values. This time around we are just going to print our struct to standard out to verify it looks as expected.

  p := &post{}
  p.title = m["title"].(string)
  p.published = m["published"].(bool)
  p.description = m["description"].(string)

  // TODO: Strip space after comma prior to parse?
  tmp := m["tags"].(string)
  p.tags = strings.Split(tmp, ", ")

  p.coverImage = m["cover_image"].(string)
  p.series = m["series"].(string)

  fmt.Printf("%#v\n", p)
}

Now there is at least one major flaw in this code we’ll need to consider - currently, it assumes that any file loaded will have the exact front matter header. This will cause it to panic if say someone tried to load a markdown file that didn’t have the series label. We’ll build out a function to check for a value and return it in the next post or so.

➜  go run main.go
&main.post{title:"test file", published:false, description:"This is the description from the test file", tags:[]string{"these", "are", "tags"}, coverImage:"https://someimage", series:"Attempting to Learn Go"}

Next Time

Here’s something to think about for next time. Using the file we loaded what would be one way to extract just the post body? We’ll cover that and revisit templates as we output our loaded markdown.


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.

{% github shindakun/atlg %}


Enjoy this post?
How about buying me a coffee?