Slack Status Updater (CLI): Setting up for dev

January 19th 2018

10:43 I have just a basic understanding of golang. I've dabbled with it in the past but I barely remember what I wrote. I definitely never did TDD with it. So this is going to be a new experience. One thing for sure though that I am going to do is work using Docker containers. I think this will really help me work through that rather awful (ahem) experience of "all my golang projects live under a single workspace". Yrch. So without further ado. Let's get setup. On the checklist:

10:52 Let's create a docker-compose file.

  • Nothing fancy. No commands. Just creating a service called clirun that mounts the volume for me. I'll be running a shell in the container it creates for the time being. Once I figure out what command I need, I'll add that to the compose file.
  • Nothing like a bit of text editor configuration to distract oneself. Trying to figure out how to make sublime text 3 have syntax specific indentation settings.
  • So according to the documentation I'll need src, bin and pkg directories. And inside src I place multiple repositories.... Oh son of a..
  • Ok. So here's what the directory looks like now.
    localscreenie-2018-01-19--11-15-03

11:13 Just discovered that golang image has a folder called /go already in it where the $GOPATH variable points to. Why commute? Changed up the docker-compose file to point to that instead

version: "3"
services:
  clirun:
    image: golang:1.9.2-stretch
    volumes:
      - "./:/go"
    working_dir: /go/src/github.com/kiriappeee/slack-status-updater-cli

11:30 Lost some blog text thanks to me forgetting that once a post is published, autosave stops working. Anyways. Just realised I wanted to build this using clean architecture. So a core package that gets imported into the CLI to use almost as an SDK. I don't have to build it this way but it's a nice way to learn the language.

slackstatusupdatercorepatterns

Oh for crying out loud. I really need to remember to press save! I just lost another set of notes from 12:05. Here's what I can remember of it. Or just the gist.

12:05 So I spun up a new repo for the core and I called it slack-status-updater-core. The plan is to use this as a library that gets imported into the CLI. The problem is that golang has no way to configure the library name other than the folder it lives in. So when I refer to the core library, I'd be referring to it as slack-status-updater-core.method which is ridiculous. Since I couldn't see anything in go that allows me to do this differently, I went and renamed the repo to ssucore which sounds terrible but it's better than typing that entire import over and over again.

And then I discovered how you can import a library and give it a name at import time. Like python's import as statement where you can import library as l and then call the library using the name l. go's equivalent to this is:

import ssucore slack-status-updater-core

Still... I'd rather have a way to reliably export the library. It means I'm not going to run into issues with any other deveopment machine because I went and cloned the thing into a different directory name by mistake.

January 22nd 2018

08:36 Day 2. And today I'm working off my laptop which means I need to get setup on this machine first. Not a bad thing. I'd have to do it eventually. The only thing that's a little irritating is setting up a structure all over again. Meh.

08:45 Alright. Dev environment is up and ready to go (and I keep reminding myself to save this post each time I type something :D )

  • Now that I'm setup I'm going to write my first formal program with TDD. fizzbuzz.

08:54 Looks like I may have been wrong about naming and package exports. It looks like in a module, I can actually have multiple packages. For each module file though I need to add a line at the top that says package <package-name> and it'll get exported eventually as package-name.a. Going to put that to the test. I'm pretty sure about the second part. I can't be sure if I can have multiple packages under a single repo though.

  • Also, looking at the testing methods. I really miss the beauty of Python and Ruby almost instantly here. This is what a test class looks like to start with:
package stringutil

import "testing"

func TestReverse(t *testing.T) {
	cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
	}
	for _, c := range cases {
		got := Reverse(c.in)
		if got != c.want {
			t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
		}
	}
}

I can't even tell where the test/assert statement is.

09:00 It looks like testify might be a more commonly used way of testing in go. Lemme confirm this by looking at a couple of libraries.

  • Turns out I was wrong. The slack library to use with go uses the standard testing package. The apex up project uses its own custom assert package..
  • Alright. Let's try and unpack the syntax in the above example line by line.
  • The first line (inside the function) is creating an array of type struct which has two fields called in and want of type string. (Tour of structs)
    • It looks like you can have a struct that doesn't necessarily have a type to it.
  • We then add the array of information. In this case it looks like this information is storing the string we want to pass in and the expected result as want. Neat.
  • The for loop is a curious one. I looked up that underscore (_,) and it's apparently called a blank identifier. I have no idea why I need it here. But I'll just go with it. The value I need is assigned to c.
  • From here it's easy. Call the reverse method on c.in. Store its result. If the result is not equal to c.want, then use a method called t.Errorf to print out an assertion error (which looks something like. Reverse of the string got this value. It should be this other value).
  • Alright. Stepping away. Need to look into the blank identifier when I'm back.

10:19 What is a blank identifier. Reading intensifies

  • According to the documentation on for loops, if I iterate over a range I get a key and value. So th _, basically discards the return value for key. But it's an array of structs so I'm not even sure what we'd get for key. I'm going to find out by writing my own little for loop that prints out both the value and key to see what I get.
  • Ah. Fascinating. For this input:
package main

import "fmt"

func main() {
    fmt.Printf("Hello World\n")
    cases := []struct{
        a, b string
    }{
        {"me", "you"},
        {"we", "they"},
        {"him", "her"},
    }

    for key, value := range cases {
        fmt.Printf("%q\n", key)
        fmt.Printf("%q\n", value)
        fmt.Printf("%q\n", value.a)
        fmt.Printf("%q\n\n\n", value.b)
    }
}

The output was:

Hello World
'\x00'
{"me" "you"}
"me"
"you"


'\x01'
{"we" "they"}
"we"
"they"


'\x02'
{"him" "her"}
"him"
"her"


Which means that the arrays are still stored with an inbuilt key. Does golang have this in their specification that arrays are dictionaries? Answer is covered in the tour over here. I really should invest some time into doing the tour at some point. The reason I haven't is because I find that tours of syntax generally doesn't get retained unless I'm actually applying it somewhere.

10:34 Well now that that is sorted, I just need to take a look at the Testing class and see what other methods are provided outside of Errorf.

Pointer what?

Before proceeding, I'm just trying to remember how pointers are used. In the case of testing in go, we seem to use a pointer to the type testing.T. But how and where does this value even get assigned? What is the value of &t? According to https://tour.golang.org/moretypes/1, go supports pointers and they work in a standard way.

p *int tells me that p is a pointer to an int type variable. What is *p and &p at this point? It should be nil. Let's test that.

var p *int
fmt.Println("%q", p)
fmt.Println("%q", *p)
fmt.Println("%q", &p)

That gives me a

%q <nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x48a019]

goroutine 1 [running]:
main.main()
        /go/src/github.com/kiriappeee/slack-status-updater-cli/hello.go:26 +0x439

Oops. So here's the question then. In the testing class we declare a function that takes a pointer to a Testing.T value. Yet we refer to it as if it's a normal variable.

//Declaration
func TestReverse(t *testing.T) {
...
...

//Usage

  t.ErrorF(....)

//As opposed to:
  *t.ErrorF(....)

So when I run this code:

func main() {
    i := 42
    readMyPointer(&i)
}

func readMyPointer(p *int){
    fmt.Println("%q", p)
    fmt.Println("%q", *p)
    fmt.Println("%q", &p)
}

I get:

%q 0xc420014088
%q 42
%q 0xc42000c030

Which is expected (minus the %q) but it doesn't tell me how t somehow magically refers directly to whatever it's pointing at.

11:13 After further reading it looks like it might have something to do with Method Declarations which this site gives a decent intro into. I suppose it's worth diving into the source code to see whether this is the case though I doubt I'll find what I'm looking for.

  • Ha! Looks like I was right
    • func (c *common) Errorf(format string, args ...interface{}) {
    • Looks like common is a struct
    • And there we have it. type T struct which carries a reference to common
  • So to test create a method which passes in a pointer to a type T struct that comes from testing and it has method declarations which are mostly made against the common type struct. The last question I have then is, how is *testing.T aware of the methods declared against its type? Is it done at compiling step? Could I declare a method anywhere in a program in any file and it'll compile it correctly?
    • I won't lie. Just reading how many ways one can get the same result makes my head spin. I mean go is probably pretty powerful. But it is ugly af. People say it's a language geared towards getting shit done by not giving you the tools to over abstract. That's great. And if it works for you, that's awesome. But on principle I like a language that balances it a bit by giving you a one true way to do something. Like what's the best way to add a method that only reads variables from a struct? Method delcaration? Function declaration? You don't know. Neither does anyone else. Flip a coin, pick that for the project. To me, that's ugly design. Of course, I'm day 2 (3?) of using go so I could just be talking through my ass.

Back to fizzbuzz.

11:38. Well. Enough rabbit holing then. Let's actually write a TDD version of fizz buzz.

  • NICE! Done it.
//fizzbuzz_test.go

package fizzbuzz

import "testing"

func TestFizzBuzzReturnsFizzWhenDivisibleByThree(t *testing.T){
    received := FizzBuzz(3)
    if received != "fizz" {
        t.Errorf("Expected fizz for FizzBuzz(3). Got %q instead", received)
    }
}

//fizzbuzz.go
package fizzbuzz

//import "fmt"

func FizzBuzz(x int) string{
    return ""
}

This gives me:

root@8eb467dedc85:/go# go test github.com/kiriappeee/slack-status-updater-core
--- FAIL: TestFizzBuzzReturnsFizzWhenDivisibleByThree (0.00s)
        fizzbuzz_test.go:8: Expected fizz for FizzBuzz(3). Got "" instead
FAIL
FAIL    github.com/kiriappeee/slack-status-updater-core 0.001s

Perfect! Let's go and actually fill in stuff I need.

11:55 All done. Now to import it and use it in a main program.

  • Just before that I finally cleared out my doubts around multiple packages and how they get imported.
    • Each folder can declare only a single package. Therefore any files inside the top level directory of a repo HAVE to share the same package name. If you want more packages, you declare them inside of a folder.
    • You then refer to these packages as import github.com/user/repo-name/package-name.
    • If your package name at the top level directory is package-a then when you import github.com/user/repo-name what you actually get is package-a as an import.

And here are the final results:

Folder structure:

localscreenie-2018-01-22--12-12-33

fizzbuzz_test.go
package fizzbuzz

import "testing"

func TestFizzBuzzReturnsFizzWhenDivisibleByThree(t *testing.T){
    received := FizzBuzz(3)
    if received != "fizz" {
        t.Errorf("Expected fizz for FizzBuzz(3). Got %q instead", received)
    }

    received = FizzBuzz(9)
    if received != "fizz" {
        t.Errorf("Expected fizz for FizzBuzz(9). Got %q instead", received)
    }

    received = FizzBuzz(12)
    if received != "fizz" {
        t.Errorf("Expected fizz for FizzBuzz(12). Got %q instead", received)
    }
}

func TestFizzBuzzReturnsBuzzWhenDivisibleByFive(t *testing.T){
    received := FizzBuzz(5)
    if received != "buzz" {
        t.Errorf("Expected buzz for FizzBuzz(5). Got %q instead", received)
    }

    received = FizzBuzz(10)
    if received != "buzz" {
        t.Errorf("Expected buzz for FizzBuzz(10). Got %q instead", received)
    }
}

func TestFizzBuzzReturnsFizzBuzzWhenDivisibleByThreeAndFive(t *testing.T){
    received := FizzBuzz(15)
    if received != "fizzbuzz" {
        t.Errorf("Expected fizzbuzz for FizzBuzz(15). Got %q instead", received)
    }

    received = FizzBuzz(30)
    if received != "fizzbuzz" {
        t.Errorf("Expected fizzbuzz for FizzBuzz(30). Got %q instead", received)
    }
}

func TestFizzBuzzReturnsNothingWhenDivisibleByThreeAndFive(t *testing.T){
    received := FizzBuzz(2)
    if received != "" {
        t.Errorf("Expected nothing for FizzBuzz(15). Got %q instead", received)
    }

    received = FizzBuzz(7)
    if received != "" {
        t.Errorf("Expected nothin for FizzBuzz(30). Got %q instead", received)
    }
}
fizzbuzz.go
package fizzbuzz

func FizzBuzz(x int) string{
    stringToReturn := ""
    if x%3 == 0{
        stringToReturn += "fizz"
    }
    if x%5 == 0{
        stringToReturn += "buzz"
    }
    return stringToReturn
}
hello.go (program runner)
package main

import (
    "fmt"
    "github.com/kiriappeee/slack-status-updater-core/fizzbuzz"
)

func main() {
    for i := 1; i <= 100; i++ {
        fmt.Printf("%d %s\n", i, fizzbuzz.FizzBuzz(i))
    }
}
Results of running

localscreenie-2018-01-22--12-14-14


With that we are ready to dive into actually developing things with go :)

This post is part of a series of logs exploring how I'm building a CLI for updating my slack status. You can follow the full project here

Posted on January 19 2018 by Adnan Issadeen