4
\$\begingroup\$

I am trying out some Go code examples (while coding a private project) to get more "in-depth" knowledge about the language.

I have come across an exercise from the Go Tour website about displaying correctly an IP number, making use of type method and the Stringer interface.

I found two ways to achieve the goal but I am wondering if there is no other clean way of doing it.

I strongly believe that a method with fewer code lines is always better - even though, looking around some Go OpenSource projects differs from that!

Example 1 with range.

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {
    rs := ""

    for k, v := range ip {
        if k == 0 {
            rs += fmt.Sprintf("%v", v)
            continue
        }

        rs += fmt.Sprintf(".%v", v)
    }

    return rs
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

Example 2 with a simple fmt.Sprintf

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {
    return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

What do you guys suggest?

\$\endgroup\$
  • \$\begingroup\$ "I strongly believe that a method with fewer code lines is always better." Really?! \$\endgroup\$ – peterSO Jul 19 at 0:30
  • \$\begingroup\$ Yes totally! For me, one of the most painful tasks is to dig into legacy code with methods +50 lines that do 125 tasks controlled by another 66 "if" statements ... \$\endgroup\$ – Guilherme Ferreira Jul 20 at 7:52
1
\$\begingroup\$

Your first variant is unnecessary detailed and hard to read:

  • an IPv4 address consists of exactly 4 digit groups
  • 4 is a small number, maybe it's worth to inline the for loop
  • the for loop treats the case k == 0 specially
  • the += operator for strings allocates a new string each time
  • the code is much longer than the description in the task "join the four numbers with dots"

Your second variant is short, easy to grasp and probably efficient enough for all practical cases.

func (ip IPAddr) String() string {
    return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}

You can replace the %v with %d to make it more specific, since the arguments are integer values.

The main reason for choosing this variant is readability. The format string shows at a glance how the formatted string will look like.

If you want to write the fastest possible code, you should probably allocate a byte array of size 3+1+3+1+3+1+3 and append each number and dot individually, without using a loop at all.

\$\endgroup\$
  • \$\begingroup\$ Did you use Go benchmarks to measure eficciency? \$\endgroup\$ – peterSO Jul 19 at 5:24
  • \$\begingroup\$ No, because speed is probably not the most important thing to measure in this case. This is a standalone program, not part of the standard library. And even if it were, it's not the code that gets called most often. \$\endgroup\$ – Roland Illig Jul 19 at 5:26
  • \$\begingroup\$ What is the unnecessary detail in the first variant? \$\endgroup\$ – peterSO Jul 19 at 8:40
  • \$\begingroup\$ The for loop is unnecessary since it makes the code much longer and more complicated than necessary. The special case action for k == 0 should be extracted from the loop. The string being built consumes another few tokens. \$\endgroup\$ – Roland Illig Jul 19 at 12:26
0
\$\begingroup\$

an exercise from the Go Tour website

I found two ways to achieve the goal but I am wondering if there is no other clean way of doing it.

Example 1 with range.

Example 2 with a simple fmt.Sprintf


Another way (Example 3):

package main

import (
    "fmt"
    "strconv"
)

type IPAddr [4]byte

func (ip IPAddr) String() string {
    s := make([]byte, 0, (1+3)*len(IPAddr{}))
    for i, b := range ip {
        if i > 0 {
            s = append(s, '.')
        }
        s = strconv.AppendInt(s, int64(b), 10)
    }
    return string(s)
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

Playground: https://play.golang.org/p/HQPd8cVAg-U

Output:

loopback: 127.0.0.1
googleDNS: 8.8.8.8

Yet another way (Example 4):

package main

import (
    "fmt"
    "net"
)

type IPAddr [4]byte

func (ip IPAddr) String() string {
    return net.IP(ip[:]).String()
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

Playground: https://play.golang.org/p/p3XKFFMBVI-

Output:

loopback: 127.0.0.1
googleDNS: 8.8.8.8

Go programmers strongly believe in meaningful code metrics. For example, performance.

A Go benchmark for Example 1:

$ go test string1_test.go -bench=. -benchmem

string1_test.go:

package main

import (
    "fmt"
    "testing"
)

type IPAddr [4]byte

func (ip IPAddr) String() string {
    rs := ""
    for k, v := range ip {
        if k == 0 {
            rs += fmt.Sprintf("%v", v)
            continue
        }
        rs += fmt.Sprintf(".%v", v)
    }
    return rs
}

func BenchmarkString1(b *testing.B) {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for N := 0; N < b.N; N++ {
        for name, ip := range hosts {
            fmt.Sprintf("%v: %v\n", name, ip)
        }
    }
}

Running the same benchmark for all four examples:

BenchmarkString1-8    841730   1412 ns/op   176 B/op   19 allocs/op
BenchmarkString2-8   1228399    931 ns/op   128 B/op    8 allocs/op
BenchmarkString3-8   1765022    626 ns/op   128 B/op    8 allocs/op
BenchmarkString4-8   1984806    574 ns/op   128 B/op    8 allocs/op

Examples 1 and 2 are inefficient.

\$\endgroup\$
  • \$\begingroup\$ Ok, I got the point with performance but, for instance, you are using in Example 4 the package "net", which by the question context and my experience, unfortunately, I had no idea about (thanks for it!). In case that I want to be focused (as you) on performance, how normally do you proceed? You write few version of the same code, run a benchmark and pick the fastest? Curious to know how Go Programmer goes into the subject... \$\endgroup\$ – Guilherme Ferreira Jul 20 at 7:58

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.