Writing a blog in go!
vityavv (30)

Preface

I started this tutorial before the contest opened, but because of school and other complications I was only able to post it today. I hope you see how much effort I put into it and give it an upvote, even though I'm probably submitting too late to win.

Note

The repl at the bottom might not work, for reasons that will become clear to you if you follow the tutorial. However, don't worry, as the code in it is still functional, and if you follow the tutorial in your own repl you'll be able to make it work!

Anyway, without further adieu, here it is!

If you have trouble viewing this on repl.it, I also have it in a Github Gist

Writing a blog in go!

CODE

About this tutorial

  • Why go? - Go is a really fast and underappreciated programming language. It is surprisingly simple, especially compared to other low-level languages like rust and c++, and it's very very easy to make web servers in it
  • What is this tutorial based on? - This is based on scms, a CMS I wrote a while ago that is extremely simple
  • Will this tutorial cover all of the features in scms? - Since this is only a short, one-part tutorial, it will not cover the API, SQL database, Markdown (you can't install go packages yet, anyway), or drafts.

Before the tutorial

Before the tutorial, you should have a basic understanding of Go. Tour of Go should help you out!

Getting started - managing articles

First, let's make a file to manage our articles.

Each article will be a json file that looks like this:

{
	"title": "The title of the article",
	"content": "The content of the article"
}

So, let's make a struct in our new file, articles.go, to reflect this:

type Page struct {
	Title string `json:"title"`
	Content string `json:"content"`
}

You may be wondering "Why can't we just have title string and content string? Well, json.Unmarshal (we'll talk about this function in a bit) only puts values into capitalizd struct values, so we have to make them capitalized and then use tags to tell json.Unmarshal what to put there. Anyway, before we begin making our functions, we have to get one more thing out of the way:

var pages map[int]Page = make(map[int]Page)

One option when making a blog like this is to load the json file each time you get it. However, we're smarter than that. We can store each article in memory beforewards to make the application much faster, and easier to make. As a downside, the more articles you have, the more memory you're going to use. However, articles are just text, which should not take up that much space. Another concern here is why I used map[int]Page instead of []Page. I did this because each article will have an ID as it's name... and if an article gets deleted, or if they're somehow out of order in the next function, it will be harder to compensate. Even if you decided to make a slice and just skip the deleted files, imagine this scenario: someone has three articles, 1.json, 2.json, and 3.json, but they delete 2.json. There's a bunch of links to the third article, but they all break because it gets deleted from the slice! However, if map is used, you can avoid this problem.

Finally, we can get to making our first function, and the most important of them all: the FileInit function. The FileInit function should be run at the beginning to load each article into the pages map we made earlier. Let's break it down to it's basic parts.

The first couple of lines are:

files, err := ioutil.ReadDir("./articles")
if err != nil {
	log.Fatal(err)
}

This is a new import! Make sure you have your file set up correctly, with package main at the top, and then in your imports add io/ioutil, used for reading and writing files. Now, let me explain what these lines do: they get a list of the files inside of the articles directory, or folder. The next three lines are basic error handling, which also use the log package.

For the rest of the tutorial, I will be ommitting error handling for sake of brevity. Every time you see a variable called err, assume that following that line is error handling as shown above.

After we get that list, we do this:

//for every file in the articles folder
for _, file := range files {
	//The following line reads the file we are on
	pageFile, err := ioutil.ReadFile("articles/" + file.Name())
	//Notice the err above, which means that in the real code I did error handling after it
	//This line makes a new page, called page
	var page Page
	//This line "Unmarshals" the json found in the file we read into the page. There is error handling after this one too
	err = json.Unmarshal(pageFile, &page)
	//In this following line, we get the page number. This includes a new import, "strconv", which I use to convert strings to numbers and vice versa. Here, I get the file name, take away the ".json" at the end, and then convert it to a number (int).
	pageNum, err := strconv.Atoi(file.Name()[:len(file.Name())-len(".json")])
	//finally, I add the page to our map
	pages[pageNum] = page
}

I've annotated the code so you can read through it, but basically it reads each file and puts it in the pages map. That's pretty much it, for the FileInit function!

Our next function will be a function called GetFrontPage. The front page of our blog will have our five most recent articles on it. Here it is, annotated:

//The function doesn't take anything, but it returns a slice of pages. The reason you see (fpPages []Page) in the return is because it already defines fpPages in the begining of the function, and then I can just type "return", without anything, and it will return the fpPages variable. This is a really cool feature of go
func GetFrontPage() (fpPages []Page) {
	//make a new slice of pages, with the *capacity* to hold 5, but a length of zero. This is some weird memory managament wizzard magic I saw online, but lengths and capacities are covered extensibly in the tour of go (have you read that yet? ;)
	fpPages = make([]Page, 0, 5)
	//Woah! Where did this getPageNumbers come from? I'll explain, right after this
	pageNumbers := getPageNumbers()
	//this for loop counts down backwards from the last element in the page numbers to the fifth-to-last
	for i := len(pageNumbers) - 1; i > len(pageNumbers) - 6; i-- {
		//Sometimes there's less than 5 elements, so we have to make sure that the page actually exists
		if i >= 0 {
			//this uses the built-in append to add the new page to the fpPages slice
			fpPages = append(fpPages, pages[pageNumbers[i]])
		} else {
			//in case it doesn't exist, it just adds an empty page struct, which makes everything the nil value.
			fpPages = append(fpPages, Page{})
		}
	}
	//as discussed earlier, I just have to type return, since the computer already knows that I'm returning fpPages
	return
}

But where did this function, getPageNumbers, come from? Well, as discussed earlier, we don't always have the page numbers as 1, 2, 3, 4, and 5. Sometimes they're out of order, and with gaps in them. So, I wrote a helper function to get me the page numbers. Have a look:

//As before, using implicit returning by already defining pageNumbers
func getPageNumbers() (pageNumbers []int) {
	//As before, magic wizzardry. This is actually pretty similar to get front page. I make pageNumbers into a slice of ints, the capacity equal to how many pages are there
	pageNumbers = make([]int, 0, len(pages))
	//when using range with maps, you do "key, value := range <map>". Here, I only need the keys, so I can omit the values.
	for key := range pages {
		//Pretty simple: I add the key to the pageNumbers slice
		pageNumbers = append(pageNumbers, key)
	}
	//New import! "sort" sorts stuff, as you can probably guess. This basically sorts the numbers.
	sort.Ints(pageNumbers)
	//Implicit return! Yay!
	return
}

We only a couple of functions left. An imediately obvious one is the function to get a single article:

//here, it takes the id of the article and returns two things: the page, and the error. This is standard practice for how go handles errors
func GetArticle(id int) (Page, error) {
	//make sure the page exists with this one simple trick!
	page, exists := pages[id]
	if !exists {
		//Another part of error handling: if there's an error, return the nil value for the first return and the error for the second. This line also includes a new import - "errors" - to make errors extremely easily
		return Page{}, errors.New("Page not found!")
	}
	//finally, we return the page that exists and nil (the null value for an error) as the error, because there's no error
	return page, nil
}

Another obvious one is to get all of them, so we can have links to them! However, this one is blatently obvious.

func GetAllArticles() map[int]Page {
	return pages
}

And finally, we come to our missing links. This function is called CreateArticle, and it creates a new article, writes it to the file, and adds it to our pages array. Here it is, annotated:

//This function takes the title and the content of the article
func CreateArticle(title, content string) {
	//Here, we create a new page, with the title and content aptly set
	newPage := Page{
		Title: title,
		Content: content,
	}
	//Here, we make a json string out of our new page. Note the "err", which means that I had error handling after this line but omitted it
	json, err := json.Marshal(newPage)
	//Here, we get our page numbers, from before. They're already sorted!
	pageNumbers := getPageNumbers()
	//Finally, we get our new page number, by taking the last page number and adding one to it.
	newPageNumber := pageNumbers[len(pageNumbers)-1] + 1
	//We add the page to our pages map
	pages[newPageNumber] = newPage
	//Finally, we add the json to our article, converting the page number to a string and putting it in the right format. The 0600 you see there is for permissions. It basically means that the person who made the file can read and write to it, and nobody else. This is the same number that the wiki tutorial uses, by the way. Also, notice the "err"
	err = ioutil.WriteFile("articles/" + strconv.Itoa(newPageNumber) + ".json", json, 0600)
}

And our final function, much simpler, is to remove an article. Here it is:

//Notice here that we return the error type
func DeleteArticle(id int) error {
	//When you assign <map>[<key>] to two values, the second value will contain a boolean saying whether the first one exists or not. We can use this to check if we have our article, and if not, return a new error saying "Article not found"
	if _, exists := pages[id]; !exists {
		return errors.New("Article not found")
	}
	//delete is built in to go. It is used to delete things from maps. Our pages variable is a map[int]page, so taking the int id and deleting it from pages would delete the article
	delete(pages, id)
	//Here, instead of doing the traditional "err := os.Remove(...)", we can instead return it, since we know that os.Remove returns an error. We can let whoever is using the function (which is going to be us, coincedentally) deal with it instead
	return os.Remove("articles/" + strconv.Itoa(id) + ".json")
}

And with that, we are done with our articles file!

Part two: Serving the pages

Part 2.1: The templates

Go is a wonderful programming language for so many reasons, but one of them is that it has built in HTML templates! With that in mind, I created three different templates for the three main pages that will go into our blog, using Go's html/template module. In part 2.2, I'll talk about how I use these, but before I do that.

By the way, if you're viewing these files in the GitHub Gist, they are just called blahblahblah.html, but in the repl, and the final application, all of them are in the templates folder.

First, we have the front page. As discussed previously, the front page has the five latest articles on it. To do that, I have this code:

<!DOCTYPE html>
<html>
	<head>
		<title>My blog!</title>
	</head>
	<body>
		<h1>My blog!</h1>
		<hr>
		<!-- Here, we use range, because we pass a slice of articles to the template. This basically means that for everything between {{range .}} and {{end}}, "." will be defined as the article that we are on. -->
		{{range .}}
			<!-- Here, we make sure that the article exists by making sure it has a title. Previously, if we had less than five articles, we would put null articles in there, so this is to make sure that we don't have a bunch of extra space at the end of our page -->
			{{if ne .Title ""}}
				<!-- here, "." is defined as an instance of our Page type, so we can just access its properties like this -->
				<h2>{{.Title}}</h2>
				<p>{{.Content}}</p>
				<hr>
			{{end}}
		{{end}}
		<a href="/archive">See all articles</a>
	</body>
</html>

Second, is our archive, which lets us see links to every single article. Since we use GetAllArticles() here, and that returns a map, we can use the map keys to provide links to each article.

<!DOCTYPE html>
<html>
	<head>
		<title>My Blog - Archive</title>
	</head>
	<body>
		<h1>My Blog - Archive</h1>
		<ul>
			<!-- Here, we have an unordered list using range. The reason we don't just have {{range .}} is because we need the key too, for the link -->
			{{range $key, $value := .}}
				<li><a href="/articles/{{$key}}">{{$value.Title}}</a></li>
			{{end}}
		</ul>
		<a href="/">Back home</a>
	</body>
</html>

And finally, our simplest page, the article page, which shouldn't really need annotation:

<!DOCTYPE html>
<html>
	<head>
		<title>My Blog!</title>
	</head>
	<body>
		<h1>{{.Title}}</h1>
		<p>{{.Content}}</p>
		<a href="/">Back home</a>
	</body>
</html>

That's it for our templates!

Part 2.2: Serving

Now that we have our files, we have to serve them to the user through our webpage! We do this with the help of one very special package, net/http! In clasical low-level languages, the default http solution is usually either non-existent or very hard to use. However, with go, it is actually quite easy to use net/http, even easier than express for node.js in some cases (e.g. built-in form parsing). Anyway, it handles a lot like express, but if you don't know express, don't worry about it, because I will be going through each line of code, step by step.

The file we will be writing to is main.go. Our first function will be the simplest and most important---the main function

func main() {
	//Initialize our files. Covered in part one, we need to put this at the top so it caches (loads) all of the files
	FileInit()
	//We add a *handler*, more on this in a sec, for any url that starts with /articles/. This includes /articles/1, /articles/2, and /articles/abacabadabacaba. The handler is articleFunc, a function which we will also discuss shortly
	http.HandleFunc("/articles/", articleFunc)
	//We add a handler for anything starting with "/", that doesn't start with "/articles/", and that handler is httpFunc.
	http.HandleFunc("/", httpFunc)
	//finally, we open up the server on port 8080. In a real environment, you'd use 80 for http. However, since we are using repl.it (or if you're simply testing this on your computer), we put any number we want above 1000. 8080 is a common testing number, as are 3000 and 8000. We use log.Fatal here (log is an import!) so that if http.ListenAndServe returns an error, we can stop the program and output the error.
	log.Fatal(http.ListenAndServe(":8080", nil))
}

while this is a simple function, it packs a lot of information. Let's look at http.HandleFunc. http.HandleFunc will set a function as a handler, meaning that it will call that function when the specified url is encountered. Since we have "/articles/" set to articleFunc, every time the server gets a request for /articles/..., it will call articleFunc with its parameters. If the request doesn't start with /articles/..., it will use the next one, which in our case is /, the catch-all, and it will call httpFunc. Here's the two functions:

//this func has to take the http.ResponseWriter (the thing we use to respond to the request) and a pointer to http.Request (the thing with all of the information from the request) as arguments, and returns nothing, as defined by http.HandleFunc
func articleFunc(w http.ResponseWriter, r *http.Request) {
	//first, we get the article number. we do this by getting the URL and taking the "/articles/" part away from it
	num := r.URL.Path[len("/articles/"):]
	//Then, we see if the last character is "/", and if so, we remove it. We use single quotes here because when we access a single character of a string, it turns into a uint8, and we can convert single characters to uint8s by using single quotes around them.
	if num[len(num) - 1] == '/' {
		//Subtracting the last element, a slash
		num = num[:len(num) - 1]
	}
	//here, we convert the string to a number. If you go to /articles/1, you're fine, but if you go to /articles/abc, the function errors, leading us to the next if statement
	pageNum, err := strconv.Atoi(num)
	if err != nil {
		//I chose not to omit this one because here instead of log.Fatal, we use http.NotFound, giving it our w and r.
		http.NotFound(w, r)
	}
	//Get the article from previous
	page, err := GetArticle(pageNum)
	if err != nil {
		//the only error that returns is "Page not found" so we can safely assume that there's a 404
		http.NotFound(w, r)
	} else {
		//then, we simply execute the templat---wait a sec, executing templates? We didn't talk about this yet! Well, hang on, and in just a sec I'll show you this wizardry.
		executeTemplate(w, "article.html", page)
	}
}

And here's httpFunc, the simpler one:

func httpFunc(w http.ResponseWriter, r *http.Request) {
	//first, we make a switch, a more efficient set of if statements
	switch r.URL.Path {
		//"/" and "/index.html" are both the same thing, so we do the same thing for them
		case "/", "/index.html":
			//oh, there's that pesky executeTemplate function again! I promise I'll get to it, just hang tight! Anyway, our front page uses the GetFrontPage function.
			executeTemplate(w, "frontPage.html", GetFrontPage())
			//finally, we return out of the case, to end the function.
			return
		//pretty much the same thing as above
		case "/archive", "/archive.html":
			executeTemplate(w, "archive.html", GetAllArticles())
			return
	}
	//Finally, if we haven't returned, that means that our thing was not found, so that's exactly what we do: error!
	http.NotFound(w, r)
}

Now I've got you hooked---surely, you are wondering "What is this executeTemplate function? How does it work?!?"---here, we use another wonderful built-in method of Go: the built in HTML Templates! Wait... we've heard this one before, haven't we? Well here we are, putting our wonderful templates to good use. We start with this line, at the beginnning (after all of the imports, of course)

//make sure you import "html/template"
var templates = template.Must(template.ParseGlob("./templates/*.html"))

Basically, with this line, we make a templates variable and set it to all of the templates in our templates folder (ParseGlob). The template.Must part is basically just a convinient wrapper around it saying that if there's an error, the app should exit immediately with that error. Since this happens at the very start and at no other time, this is OK! Anyway, let's look at our executeTemplates function to see how we managed to pull this off:

This function takes three parameters. It needs the http.ResponseWriter from our http funcs so that it can write the response to them. It also needs to know what template is being executed. Finally, it needs the content. Since the content and template are different each time, we use an "interface{}", meaning we don't really know the type. In fact, we don't have to know the type at all, because ExecuteTemplate takes a "interface{}" for its content, so as long as we match everything up when we call the function, we should be fine.

//I was going to put the above paragraph right here as a comment but I realized it was getting too long
func executeTemplate(w http.ResponseWriter, templ string, content interface{}) {
	//We use templates.ExecuteTemplate() to execute the specific template we want out of the ones we loaded. You can see how this is used in the useage of the function in previous functions.
	err := templates.ExecuteTemplate(w, templ, content)
	if err != nil {
		//Here, instead of killing the server, we give them the error, and a 500 internal server error.
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

That's pretty much it for our main.go file... so far...

Part 3 - Administration!

This is our final and hardest part, and that is being able to delete and create articles without booting into the repl. Since you can't get packages for go yet, you have to do some work arounds, which I spent a lot of time finding out, so you're going to have to carefully follow these steps:

  • Make yourself an explorer (how)
  • Open the command pallete by making sure the editor is in focus and pressing F1
  • Type in shell, press enter
  • Type in: go get golang.org/x/crypto/bcrypt
  • Press enter. You might get an error, ignore it (unless it leads to further issues)

Why are we doing all this? Well, we can't just store our password in plain text! We have to make sure that it is protected securely, and the way to do that is to use the bcrypt library (there are some others you can use too, but bcrypt is pretty much the industry standard). Other than the first step, which you only need to do once, you may have to do this every time you load up your repl, because of how repl.it works, unfortunately.

Anyway, with that out of the way, let's look at how we're going to do things.

We will have a "/dashboard" page, pretty similar to our "/archive" page, but this time we will add buttons to delete articles and to create new ones. This part is pretty simple, so we can add this case to our switch inside of httpFunc:

case "/dashboard", "/dashboard.html":
			executeTemplate(w, "dashboard.html", GetAllArticles())
			return

The dashboard itself, though, will be a little bit more complicated. It utilizes javascript to make a "DELETE" request to the server when articles are deleted, but you don't have to worry about knowing javascript, because I can walk you through it:

<!DOCTYPE html>
<html>
	<head>
		<title>My Blog - Archive</title>
	</head>
	<body>
		<h1>My Blog - Archive</h1>
		<!-- Button links to the "/new" page, for making new articles -->
		<a href="/new">New article</a><br>
		<!-- same as before, with the article, except... -->
		<ul>
			{{range $key, $value := .}}
				<!-- Here, we have a button element, and when it is clicked, it calls the del function in our javascript with the paramater being our key. -->
				<li><a href="/articles/{{$key}}">{{$value.Title}}</a> | <button onClick="del({{$key}})">Delete</button></li>
			{{end}}
			</ul>
		</form>
		<a href="/">Back home</a>
		<script>
			//Here's our del function! Javascript doesn't care about types, but the equivalent in go would be "func del(key int) {"
			function del(key) {
				//The prompt function creates a dialog box asking for a password
				password = prompt("Please enter your password");
				//We create a new formData object to turn our password into formdata that go can then use
				let formData = new FormData();
				formData.append("password", password)
				//We use fetch to make the request. The key there will be replaced with whatever the key that's passed to the function is
				fetch(`/delete/${key}`, {
					//For our options, we set the method to DELETE (as to be fancy), and our body to the formData from earlier
					method: "DELETE", 
					body: formData
				//javascript async mumbo jumbo that translates to "get the text from it"
				}).then(r => r.text()).then(r => {
					//if it isn't successful then we alert the error, otherwise we reload the page to reflect the change.
					if (r !== "Article successfully deleted") {
						alert(r);
					} else {
						location.reload();
					}
				//finally, if something goes wrong, we alert that too
				}).catch(alert);
			}
		</script>
	</body>
</html>

See? That wasn't so hard. But wait, how do we handle these requests? Well, let me introduce you to the next function in our main.go file, deleteFunc. This will also introduce us to how go's bcrypt library works. To use this function, put http.HandleFunc("/delete/", deleteFunc) in your main function, anywhere above the "/" handler.

//The function is formatted like a normal http.HandlerFunc
func deleteFunc(w http.ResponseWriter, r *http.Request) {
	//Here, we get the key by removing "/delete/" from the path.
	strKey := r.URL.Path[len("/delete/"):]
	//We use strconv to convert the key to an actual key.
	key, err := strconv.Atoi(strKey)
	//If, of course, the key isn't an int, we error with a 400, meaning there was a bad request
	if err != nil {
		http.Error(w, "That's not a valid key", http.StatusBadRequest)
		//And stop executing
		return
	}
	//Now, if the method is DELETE (which it should be)...
	if r.Method == "DELETE" {
		//We make sure that the form sent has a password value. If not, we error, again with a 400.
		if r.FormValue("password") == "" {
			http.Error(w, "Password is missing", http.StatusBadRequest)
		//Otherwise...
		} else {
			//Remember this line. This is how we use bcrypt to check passwords. Also, remember "pwHash," because we'll talk about that in a second. Anyway, as you can probably guess, this converts the two strings to byte slices before comparing them, because that's what bcrypt uses
			err := bcrypt.CompareHashAndPassword([]byte(pwHash), []byte(r.FormValue("password")))
			//Instead of returning a boolean, bcrypt will return an error if they don't match. http.StatusUnauthorized, yet another constant, is 401.
			if err != nil {
				http.Error(w, "Password does not match", http.StatusUnauthorized)
			} else {
				//Finally, the user has been authenticated, and we can use our DeleteArticle function from earlier to delete the article with that key
				err = DeleteArticle(key)
				if err != nil {
					//If you look back to our DeleteArticle function, you'll remember that we error with Article not found when an article isn't found. Now, we can check this!
					if err.Error() == "Article not found" {
						http.Error(w, "Article not found", http.StatusNotFound)
					} else {
						http.Error(w, "Internal Server Error", http.StatusInternalServerError)
					}
				} else {
					fmt.Fprint(w, "Article successfully deleted")
				}
			}
		}
	} else {
		http.Redirect(w, r, "/", http.StatusFound)
	}
}

If you try running that code in it's current state, you will notice that it errors. In fact, it will say that pwHash is not defined! So, let's fix that.

  • First, go to a website that generates bcrypt hashes, and put in the password you want to do. Here's a site that does it!
  • Then, make a .env file in your repl.
  • Inside of that file, put PASSWORD=<your bcrypt hash>, where <your bcrypt hash> is replaced with the hash that the website generated
  • Last two steps! Put var pwHash = os.Getenv("PASSWORD") at the top of your file, and...
  • Put the following code at top of your main function
if pwHash == "" {
	log.Fatal("There is no password set! Please create a file called .env and make the contents \"PASSWORD=asdf\", with your password bcrypt hashed instead of asdf ")
}

Now, if you've done all of these steps correctly, you should have a working dashboard, where you can delete articles! But, there is one more thing. We need to be able to add new pages as well! Let's first make a page, called new.html, where the user can put in a new article:

<!DOCTYPE html>
<html>
	<head>
		<title>My Blog - New!</title>
	</head>
	<body>
		<h1>New Article</h1>
		<!-- This <form> tag makes it so that when someone clicks the submit button, it makes a POST request to the /new page with the information in the form -->
		<form method="POST" action="/new">
			<label for="title">Title</label>
			<!-- Here, we use the required attribute to make sure that the user inputs it. However, this is not enough! We also have some checks on the server side which make sure the title (and password) are sent -->
			<input name="title" type="text" required><br>
			<label for="content">Content</label><br>
			<textarea name="content" rows="20" cols="100" required></textarea><br>
			<label for="password">Password</label>
			<input name="password" type="password" required><br>
			<!-- When the user clicks on the following button, the browser will make a POST request to the server to make the new article! -->
			<button type="submit">Submit</title>
		</form>
	</body>
</html>

Finally, we have to handle the article. However, you might notice that I made the form make a POST request to /new. Isn't the page called /new.html? When we handle this, we're going to check the request type. If it's a post request, we process it. If it's any other type of request, including a GET request, we will send the page. Here's how we handle it, the final part to our program:

		//You may notice that we are indented, and it starts with a case statement. This is because this part goes into our main "httpFunc" from earlier.
		case "/new", "/new.html":
			//Here's where we make the aforementioned check
			if r.Method == "POST" {
				//We have to make sure that the form actually sent over all of the information. r.FormValue("thing that wasn't sent") returns an empty string, so we can check on that
				if r.FormValue("content") == "" || r.FormValue("title") == "" || r.FormValue("password") == "" {
					http.Error(w, "Either the content, title, or password are missing", http.StatusBadRequest)
				} else {
					//Assuming it passes, we move on to the next step, checking the password, just like last time in our deleteFunc
					err := bcrypt.CompareHashAndPassword([]byte(pwHash), []byte(r.FormValue("password")))
					if err != nil {
						http.Error(w, "Password does not match", http.StatusUnauthorized)
					} else {
						//Finally, we create our article.
						CreateArticle(r.FormValue("title"), r.FormValue("content"))
						//And send the user to the front page
						http.Redirect(w, r, "/", http.StatusFound)
					}
				}
			} else {
				//If you hop back up to where that { was opened, you'll see this was right after "if r.Method == "POST" {", so this is the part where we serve the page, as the request was *not* a POST but rather a GET (or something else, we don't care)
				executeTemplate(w, "new.html", []string{})//this last one doesn't matter, we aren't using anything in the template
			}
			return

And that's it! We are done with our Blog!

Next Steps

I left a challenge in the tutorial! Take a look at DeleteArticle and GetArticle in our articles.go file, and see how they differ from the other functions in that file. They both return an error as their last (or only) return value! Your goal is to reformat all of the other functions to return an error as well, instead of using log.Fatal(), which kills the blog. Finally, every time these functions are used, make it so that if there was an error, it returns an error to the client with an HTTP code 500 (Internal Server Error), like in our executeTemplate function (main.go) or our deleteFunc function (main.go).

You are viewing a single comment. View All
timmy_i_chen (946)

This is pretty awesome, thanks for making it!