Skip to content
Sign upLog in
← Back to Community

Writing a blog in go!

Profile icon
vityavv

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).

Voters
Profile icon
VanceBakalov
Profile icon
TheOceanFace
Profile icon
Vutuner
Profile icon
benhudson515
Profile icon
Makrayne
Profile icon
abc3354
Profile icon
unsettledtax7
Profile icon
umashd7
Profile icon
JohnCouto
Profile icon
Madhumathi_e
Comments
hotnewtop
Profile icon
timmy_i_chen

This is pretty awesome, thanks for making it!

Profile icon
isakkeyten

I get

exit status 1 main.go:9:2: cannot find package "golang.org/x/crypto/bcrypt" in any of: /usr/local/go/src/golang.org/x/crypto/bcrypt (from $GOROOT) /go/src/golang.org/x/crypto/bcrypt (from $GOPATH)
Profile icon
Joseanthony1

Looking for a reliable online assignment help provider to help in the UAE? Don’t worry we got the solution to your problem. We offer all academic assignment services for students within the budget. Visit our website to learn more.

Profile icon
samswilson

If you are finding the most updated and 100% authentic HP exam dumps, then you have come to the right website. Pass HP certification is a difficult challenge for https://www.dumpswrap.com/ all IT students. Manu students fail their certification exam due to lack of knowledge or using outdated preparation materials. Dumpswrap here’s to help you to ensure your preparation and success in first attempt. If you want to make your preparation best and for your HP certification exam perfect, then select our most updated HP exam dumps preparation materials.

Profile icon
JuliaHudson1

The subject matter contains the most credible data for optimization. Nursing assignment help uk can provide you guidance for the composition of your projects.

Profile icon
HafsahOmar

This information is very helpful for me. I am an academic writer and my website is assignment help Dubai, thank you for share this very informative post for all.

Profile icon
CharlieWaylon

All information is useful for me because as a cheap assignment writer USA, I am working in the writing field so I really need this type of information that is helpful for me.

Profile icon
Oliversmith33

Thanks to you, I've been searching how to write blogs and articles on the GO! Replit & Essays UK, who also have written a complete guide on how to write articles & essays. You people are time saviours. Keep up the good work.

Profile icon
JenniferPauli

Wow, you did an incredible job. Everything is described in such detail, I will definitely use this when creating my own blog. I recommend reading one of my articles about quoting famous people, it is very interesting and informative.
I will follow your posts, you are a very talented person. I love it when people create something useful for others.

Profile icon
JaneJLocane

Writing a blog or article is not an easy task. I had problems with this both at school and at the university. Essay Geeks helped me when I had to submit my essays and term paper in one deadline. Thanks to this, I was able to properly plan my time. And I didn't have to sit for days reading materials.

Profile icon
Michelkong

Amazing post it is very impressive and inspiring article thanks dude for sharing with us Black Patched Varsity Letterman Bomber Jacket

Profile icon
Elizabeththy

Such a great work, thank you for all the details!

Profile icon
JackyApril

For many students, the process of how to choose a good essay service can be a confusing one. You might even think that there is no way to make this decision. You have read many great reviews from students who have used some of the most well-known essay writing services. Yet, you have also heard from many students who are less fortunate in having such a stellar support. Therefore, it seems to me that it is better to trust my advice and give preference to https://supremedissertations.com/ because it has good reviews.