13
\$\begingroup\$

Here's my first serious attempt at writing Go code. This program counts the number of days until a certain date and displays that in the system tray.

To do that, it first reads the target date from a file — containing, for example, Dec 25, 2016 — and counts the number of days inbetween, then writes that number to an image and saves it as an .ico file, and finally displays that icon in the tray. (That means the countdown won't be updated unless you restart the program, but I don't think that's necessarily an issue.)

I've got to say, though, for a program that does so many different things it was relatively little code for me to write! I'm pretty pleased with the language so far.

As this is my first Go, I expect to be breaking a few conventions. In particular, I'd appreciate feedback on the following:

  • Am I splitting things up right? I made a separate function for every major step, and I'm calling them in order in main. I guess I could have made them packages instead, but they seem rather too specific for that and all the hard parts are already encapsulated in packages anyway.
  • How's my error handling? Right now I'm checking err whenever possible, and return if I'm in main and pass the error on otherwise. Is this how it's meant to be done? That seems a little verbose to me.

Any other kind of input would be much appreciated as well.

package main

import (
    "bufio"
    "errors"
    "fmt"
    ico "github.com/Kodeworks/golang-image-ico"
    "github.com/golang/freetype/truetype"
    "github.com/xilp/systray"
    "golang.org/x/image/font"
    "golang.org/x/image/math/fixed"
    "image"
    "io/ioutil"
    "math"
    "os"
    "strconv"
    "time"
)

const (
    dateFormat         = "Jan 2, 2006"
    targetDateFilename = "targetDate"
    iconFilename       = "icon.ico"
    fontFilename       = "micross.ttf"
    iconSize           = 16
    hoursPerDay        = 24
)

func main() {

    durationDays, err := getDurationDays()
    if err != nil {
        fmt.Println(err)
        return
    }
    durationDaysStr := strconv.Itoa(durationDays)

    err = saveTextToIconFile(durationDaysStr, iconFilename)
    if err != nil {
        fmt.Println(err)
        return
    }

    err = showTrayIcon(iconFilename, durationDaysStr)
    if err != nil {
        fmt.Println(err)
        return
    }

}

func getDurationDays() (int, error) {

    currentTime := time.Now()
    targetDate, err := getTargetDate()
    if err != nil {
        return 0, err
    }

    if targetDate.Before(currentTime) {
        return 0, errors.New("can't count down to a date in the past")
    }

    durationHours := targetDate.Sub(currentTime).Hours()
    durationDays := int(math.Ceil(durationHours / hoursPerDay))

    return durationDays, nil

}

func getTargetDate() (time.Time, error) {

    file, err := ioutil.ReadFile(targetDateFilename)
    if err != nil {
        return time.Time{}, err
    }

    targetDate, err := time.Parse(dateFormat, string(file))
    if err != nil {
        return time.Time{}, err
    }

    return targetDate, nil

}

func saveTextToIconFile(text string, iconFilename string) error {

    img := image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))

    // more or less taken from the example

    fontBytes, err := ioutil.ReadFile(fontFilename)
    if err != nil {
        return err
    }
    f, err := truetype.Parse(fontBytes)
    if err != nil {
        return err
    }

    d := &font.Drawer{
        Dst: img,
        Src: image.White,
        Face: truetype.NewFace(f, &truetype.Options{
            Size:    10,
            DPI:     72,
            Hinting: font.HintingNone,
        }),
    }
    d.Dot = fixed.Point26_6{
        X: (fixed.I(iconSize) - d.MeasureString(text)) / 2,
        Y: fixed.I(12),
    }
    d.DrawString(text)

    outFile, err := os.Create(iconFilename)
    if err != nil {
        return err
    }
    defer outFile.Close()

    writer := bufio.NewWriter(outFile)

    err = ico.Encode(writer, img)
    if err != nil {
        return err
    }

    err = writer.Flush()
    if err != nil {
        return err
    }

    return nil

}

func showTrayIcon(iconFilename string, tooltip string) error {

    iconPath := ""
    clientPath := "" // i don't really know what these do
    port := 6333     //

    tray := systray.New(iconPath, clientPath, port)

    err := tray.Show(iconFilename, tooltip)
    if err != nil {
        return err
    }

    err = tray.Run()
    if err != nil {
        return err
    }

    return nil

}
\$\endgroup\$
  • 6
    \$\begingroup\$ Puns! Way to Go! \$\endgroup\$ – Mathieu Guindon Nov 8 '16 at 15:02
2
\$\begingroup\$

I'm also new to this language, but I'll give it a go.

As far as splitting things up goes, you're doing great. The code reads really nicely from top to bottom, from logical unit to the next. The only thing I'd pick on is the parameterless getDurationDays in main. It's unclear what duration it will be, because the time interval boundaries are determined inside the function. It's understandable that one end of the boundary will be "now", but the other end is anyone's guess. I think the logical flow inside main would be a bit better if the getTargetDate call was there, and the result passed onto getDurationDays as parameter. At that point, "now" could also be a parameter, and that way getDurationDays would become testable.

Definitely no need to split this to multiple packages, yet.

As far as error handling goes, I think you're following the recommended conventions. Yes it's a little bit verbose, but this is considered normal in this language, and the way to go.

A minor comment on style, when multiple parameters have the same type, a shorter signature is possible, for example:

func saveTextToIconFile(text, iconFilename string) error {

Last but not least, some variable names could be better. I suggest to go with:

  • file -> content
  • f -> face
  • d -> drawer
\$\endgroup\$
  • \$\begingroup\$ Thanks for the feedback! Good call ongetDurationDays needing parameters. \$\endgroup\$ – vvye Nov 14 '16 at 20:16
  • \$\begingroup\$ The parameter remark could be extended to all your const defined at the beginning: you could use them only in your main directly (and let them be parameters of all the other functions) \$\endgroup\$ – oliverpool Jan 21 '17 at 11:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.