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:
- Get me a docker container. - Using the stretch image
- Setup a repo that does a hello world using
docker-compose
.
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 thecompose
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
andpkg
directories. And insidesrc
I place multiple repositories.... Oh son of a.. - Ok. So here's what the directory looks like now.
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.
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 standardtesting
package. The apexup
project uses its own customassert
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 calledin
andwant
of typestring
. (Tour ofstructs
)- 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 toc
. - From here it's easy. Call the
reverse
method onc.in
. Store its result. If the result is not equal toc.want
, then use a method calledt.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 arange
I get akey
andvalue
. So th_,
basically discards the return value forkey
. But it's an array of structs so I'm not even sure what we'd get forkey
. I'm going to find out by writing my own little for loop that prints out both thevalue
andkey
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
.
- Looks easy enough. Let's jump in! Fizzbuzz time.
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 tocommon
- So to test create a method which passes in a pointer to a type
T
struct that comes fromtesting
and it has method declarations which are mostly made against thecommon
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 usinggo
so I could just be talking through my ass.
- I won't lie. Just reading how many ways one can get the same result makes my head spin. I mean
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 importgithub.com/user/repo-name
what you actually get ispackage-a
as an import.
And here are the final results:
Folder structure:
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
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