Skip to content

2021🔗

Go R1 Day 63

progress

  • Did some work on Go regex processing with a new linting tool concept I have for "semantic line breaks".
  • I've forced myself to apply TDD from the get go, so it's been slow going initially to abstract my functions to be testable without being run as a CLI tool.
  • Started on the run test for simulating the cli call as well.

Go R1 Day 62

progress

  • Worked with mocks.

Still using the is package to test.

if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
  t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
}

was replaced with:

is.Equal(spySleepPrinter.Calls, want) // spySleepPrinter shows correct order of calls

Go is messing with my head with how everything gets simplified to the lowest common interface when possible. Instead of buffer, I'd want to use io.Writer for example. This abstraction is where there is so much power, but it requires a deeper understanding of following the dependency tree to know what properly satisfies the interface. I'm finding that one layer isn't enough sometimes with lower level interfaces, and requires getting comfortable with more low level packages from the standard library. Pretty cool that I didn't need to do anything more complex to do a comparison.

When To Use Mocking

Without mocking important areas of your code will be untested. In our case we would not be able to test that our code paused between each print but there are countless other examples. Calling a service that can fail? Wanting to test your system in a particular state? It is very hard to test these scenarios without mocking. 1

Other Good Insights

"When to use iterative development? You should use iterative development only on projects that you want to succeed." - Martin Fowler

I really agree with this next quote. I've seen this happen so often with the pressures of a project, and feel it's the excuse that causes the escalation of technical debt that becomes difficult to untangle retroactively.

Try to get to a point where you have working software backed by tests as soon as you can, to avoid getting in rabbit holes and taking a "big bang" approach. 1

Go R1 Day 61

progress

It was a dark and stormy night. The world was bleak. A command was sent to The Compiler.

The assumptions were flawed. The command was rejected.

You have given me an invalid command with -argument-that-shall-not-work 5 being invalid.

But I've provided --argument-that-shall-not-work, the indomitable wizard said.

Your command is unworthy.

Digging into esoteric tomes of knowledge, the wizard discovered others have had similar issues when calling external processes using the legendary wizardry of os/exec. However, none could shine light into the darkness of his failure.

Running in pwsh worked fine.

Next, the wizard tried a variety of escaping commands.

  • Using shellescape package.
  • Using back-ticks with the arguments to escape.
  • Using partially quoted arguments in the slice of the strings.
  • Using no quotes.
  • Went down the path of ancient texts describing similar issues.1

To the wizards dismay, copying the printed debug output worked fine in the terminal, but alas would not be executed by The Compiler.

It began to feel like the curse of dynamic SQL queries that had long plagued the wizard until PowerShell had been discovered.

The wizard ruminated on his plight. He thought:

At the end of the day, all things seem to come down to strings and the cursed interpretation of my textual commands to The Compiler. How many a day have I wasteth upon the fault of a single character. The root of all evil must be a string."

The wizard connected to a new remote instance, using the power of the Remote SSH plugin and began debugging in VSCode.

The debug breakpoint config that worked was set in stone.

  {
      "name": "Run frustrating-test",
      "type": "go",
      "request": "launch",
      "mode": "debug",
      "program": "${workspaceFolder}/MyTestApp/test.go",
      "args": [
        "-count",
        "100",
        "-batch",
        "10",
        "-delay",
        "1",
        "-server",
        "MyCustomIPAddress",
        "-debug",
      ],
      "debugAdapter": "legacy" // newer version wouldn't work remote
    },

Consulting The Sage (aka Debugger), it advised the wizard of the message sent to The Compiler.

Debug Variables

The wizard had a revelation. A fellow wizard advised to break the appending into individual statements instead of trying to do so much in one step.

The incantation changed from:

command = append(command, fmt.Sprintf(`--header "user-id: %s"`, petname.Generate(petNameLength, "-"))

to the following:

command = append(command, "--header")
command = append(command, fmt.Sprintf(`user-id: %s`, petname.Generate(petNameLength, "-")))
command = append(command, "--max-retry-count", "5")

The foe vanquished, the The Blight of Strings was cast aside with malice. The wizard swore to never fall prey to this again.

{{< admonition type="Note" title="Further Detail (:(fas fa-external-link-alt): expand to read)" open=false >}}

Josesh1 pointed towards: EscapeArgs. I did not find any equivalent for Darwin. The closest thing I could find was execveDarwin which I believe is the execution line, which gets the argument list from: SlicePtrFromStrings which is defined at here

I'll have to re-examine in the future when I have more experience with Go, as it's not a simple chain to follow. :(fas fa-brain):

[test --param 1]
strings.Join(a)... "test --param 1"
os.Command: ["test" "--param 1"]
echo "test --param 1\n"

This pointed towards a similar issue with the \n showing up.

:(fas fa-play): Playground

{{< /admonition >}}

The Compiler's heartless gaze felt nothing. In the shadows, The String Balrock bid its time, knowing that the wizard would suffer once again.

It Works

Go R1 Day 60

progress

  • Finished up the basics of dependency injection and how this helps with testable code.
  • Worked through concurrency test using channels and go routines. This will take a bit more to get comfortable with as there is a variety of concepts here. My initial attempts finally started working using an anonymous function, but couldn't finalize due to some issue with launching the parallel executable being called. Not sure why the --argname seemed to be trimming the first dash from the argument when using args = append(args,"--argname 5").

I sure enjoy the visuals from pterm library. When not using the -debug flag, the concurrent counter reported a nice increase of total threads and then exited upon failure of any goroutine.

Go R1 Day 59

progress

  • Built some Mage tasks for setup of local projects.
  • Used retool post from Nate Finch with Mage project and found it promising to vendor Go based tooling into a local project and not rely on the global package library.
  • Created magetools repo to house some basic general mage tasks I could share with other projects.
year, month, day := time.Now().Date()
dateString := fmt.Sprintf("%d-%02d-%d", year, month, day)

Use padding in the Sprintf call to ensure a date comes back with 07 instead of 7.

Go R1 Day 57

progress

  • Did some adhoc work in this repo (the hugo repo that contains this blog) testing out Mage, which is a Go based Make alternative.
  • Generated dynamic target directory for hugo posts using stringify for kebab case.
  • Unexpected behavior when generating dynamic file path including date.
year, month, day := time.Now().Date()
str := stringy.New(title)
slugTitle := strings.Join([]string{string(year), string(month), string(day), str.KebabCase("?", "").ToLower()}, "-")

The output failed to handle the year, resulting in some odd \x18 path generation.

In reviewing the values from returned from time.Now().Date, I realized it wasn't an int value being returned.

To work through the definition, I figured this would be a good chance to use the cli only to find the docs.

go doc 'time.Now'

returned the following:

package time // import "time"

func Now() Time
    Now returns the current local time.

To get the source code for the function:

  • go doc -src 'time.Now'
  • go doc -src 'time.Date'

This shows the return value of Date() is actually time type, not int.

Still couldn't see where the multiple return parameters were defined so I ran:

go install golang.org/x/tools/cmd/godoc@latest
godoc --http 127.0.0.1:3030

Ok... Figured it out. I was looking at the func Date(). However, what I should have been looking at was the exported method func (Time) Date. This correctly shows:

func (t Time) Date() (year int, month Month, day int)

I still couldn't figure this out until I tried running it in the playground.

:(fa-fas fa-play): Playground - Demonstrate Problem

./prog.go:11:37: conversion from int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)

Runes. Strings. I know folks say it boils down to 1's and 0's, but dang it... seems like my life always boils down to strings. 😆

:(fa-fas fa-play): Playground - Fixed The Problem

Finally got it all working.

Strongly typed languages are awesome, but this type of behavior is not as easy to figure out coming from a background with dynamic languages. I

In contrast, PowerShell would be: Get-Date -Format 'yyyy'.

Here's an example of the Mage command then to generate a blog post with a nice selection and prompt.

// calculatePostDir calculates the post directory based on the post title and the date.
func calculatePostDir(title string) string {
    year, month, day := time.Now().Date()
    str := stringy.New(title)
    kebabTitle := str.KebabCase().ToLower()
    slugTitle := strings.Join(string(year), string(month), string(day),kebabTitle, "-") ///stringy.ToKebabCase(title)

    pterm.Success.Printf("Slugify Title: %s", slugTitle)
    filepath := filepath.Join(contentDir, string(year), slugTitle)
    pterm.Success.Printf("calculatePostDir: %s", slugTitle)
    return filepath
}

Then creation of the post:

// New namespace groups the new post generatation commands.
type New mg.Namespace
// NewPost creates a new post in the Hugo format.
func (New) Post() error {
    prompt := promptui.Select{
        Label: "Select Type of Post j/k to navigate",
        Items: []string{"100DaysOfCode", "microblog", "blog"},
    }
    _, result, err := prompt.Run()
    if err != nil {
        pterm.Success.Printf("Prompt failed %v\n", err)
        return err
    }
    pterm.Success.Printf("New Post: [%s]", result)
    promptTitle := promptui.Prompt{
        Label: "Enter Title",
    }
    title, err := promptTitle.Run()
    if err != nil {
        pterm.Error.Printf("Prompt failed %v\n", err)
        return err
    }
    // the archetype in archtytpes directory to use
    var kind string

    switch result {
    case "100DaysOfCode":
        kind = "code"
    default:
        kind = result
    }
    fileName := calculatePostDir(title)
    if err := sh.RunV("hugo", "new", fileName, "--kind", kind); err != nil {
        return err
    }
    return nil
}

Go R1 Day 58

progress

  • Avoiding a panic in Go for missing dictionary match is very straight forward. The error pattern for failed conversions and out of range index matches is the same, with: ok, err := action.
  • TODO: Figure out if ok to reference an error in a test by: is.Equal(error.Error(),"unable to find value in map"). Linter warns me with: Method call 'err.Error()' might lead to a nil pointer dereference.
  • Started work with dependency injection.

Go R1 Day 55

progress

In Go, when you call a function or a method the arguments are copied. 1

  • Built some test cases for working with pointers and test methods.
  • Did this in Goland to become more familar with it and figured out how to use the nice tests explorer with a floating window to auto-test.
  • Built a filewatcher configuration watcher as alternative to tests panel, allowing automated run in terminal or output panel of go test ./....
  • Couldn't figure out how to run every test in project, as it's a multi-module repo (for tutorials). VSCode works fine with this with the multi-module support.

These pointers to structs even have their own name: struct pointers and they are automatically dereferenced. 1

This explains something that felt a little confusing as a new Gopher. Now I know why returning a value back as an int didn't require explicit dereferencing. 2

func (w *Wallet) Balance() int {
  return w.balance // <---- automatically deferenced
}

Not clear yet on if I need to set the is := New(t) in the context of each t.Run() or not.

t.Run("withdraw 2 amounts", func(t *testing.T) {
        wallet := pointers.Wallet{}
        wallet.Deposit(pointers.Bitcoin(20))
        err := wallet.Withdraw(pointers.Bitcoin(5))
        is.NoErr(err) // Withdraw should have no errors
        assertBalance(t, &wallet, pointers.Bitcoin(15))
    })

My Goland Log

Purpose

This is a log of my second journey in using Goland from Jetbrains for Go development.

I've got VSCode in a great state. It's flexible, powerful, and I've highly customized it to my workflow.

However, doing Go development, I'd like to better explore Jetbrains Goland and see if the experience proves positive.

I'll log updates and issues here as I work through it in the hope that it might provide you better information if you are considering Goland as well.

Emoji Definition
🔴 Not solved yet
🟠 Issue or behavior unclear
🚩 It's an issue I believe I understand but don't like the answer to
Understand and no issues with behavior

Goland Log

{{< admonition type="Question" title=":(fas fa-vials): Testing" open=false >}}

🔴 Not certain yet how to run multi-module repo based tests through project (VSCode supports with experimental flag).

✅ Run individual directory test. This also generates a run configuration (ephemeral) that can be persisted as a test configuration for the project.

test output

test coverage on file list

✅ Toggle auto-run for testing is possible, so upon save, it will run after a 1 second delay. The built in test renderer is better than VSCode's Go test output. Not only does it render the test with color1 but also organizes into a nice test explorer view.

Overall, the test output is polished, able to be undocked and run in a separate window. Works well for a TDD workflow.

Goland Test Coverage

@s0xzwasd provided some great insight on the different approach in the comments on this page. Compound configurations work well to run multiple sets of test when using multiple modules in the same repo. I tried this, and while tedious to click through the first time, It's easier to configure from a UI standpoint than trying to work through the VSCode tasks.json schema and build tasks by hand.

Goland Compound Test Coverage Explorer Output

{{< /admonition >}}

{{< admonition type="Question" title=":(fas fa-running): Run Configurations" open=false >}}

🔴 Not able to set dynamic $FileDir$ in the run configurations it seems.

🟠 Project configuration resolves path to relative once saved, but displays with absolute. This is confusing.

✅ Can include configuration in project so anyone that opens in Goland can run the same prebuilt tasks.

{{< /admonition >}}

{{< admonition type="Question" title=":(fas fa-search): Linting" open=false >}}

🟠 Working on golangci-lint integration. There is an extension and I've also configured the FileWatcher to run on save, but neither is working as seamlessly as VSCode setting as the gopls linter tool.

{{< /admonition >}}

{{< admonition type="Question" title=":(fas fas fa-tools): Refactoring & Fixes" open=false >}}

🟠 Can't find a way to assign intentions to a keyboard shortcut. For example, Add a Go Comment header requires a click on a lightbulb icon, and can't find a way to allow this to be triggered by a keyboard shortcut.

{{< /admonition >}}


  1. vscode is requires -nocolor type argument to avoid console escaping if using a library that uses color.