intro

For those who do not know, ft is a new project of mine that revolves around a rather novel idea: a remote file browser. The motivations behind this project are written rather inarticulately in this article. Today, I’d rather write about the challenges of using Go and the common pitfalls I used to fall into while building a project.

go’s weaknesses

Go has a lot of strengths but also a lot of weaknesses, these weaknesses are common in most languages, or if not present, those other languages have bigger weaknesses because they avoided having Go’s weaknesses. Let me explain.

Go is rigid. No doubt about it. For starters, you cannot define two functions with the same name but with different data types. A second con of using go is that you cannot take in a slice(Go’s version of array) with an unknown type, even if you won’t read the unknown type. Lastly, making data structures directly inherit another data structure’s properties isn’t easy.

You’d think that in 2013(the year Go was made) those issues would be solved already, if not now since Go is 9 years old. The Go developers prioritization to make simplicity the core trait of Go has made it a lot less powerful compared to language like C++ or Common Lisp.

Before I go into how I dealt with those issues with my project ft, I want to first talk about languages that solve these problems elegantly so that the Go developers get inspired.

functions with same name

In C++, there is a feature called function overloading. What function overloading does is allow two functions of the same name but with different parameter types or return values. So, in Go, you’d have something like:

func printVal(s io.Writer, b []byte) {
    s.Write(b)
}

func printVal(s io.Writer, b string) {
    s.Write([]byte(b))
}

That is one way to do it. Another way would be to define a generic function just like how Common Lisp does it. Common Lisp’s CLOS system allows for generic functions that can be implemented for different types, so that when you call printVal, for example, it calls for the string if provided a string or a slice of bytes.

Let’s see it in action:

(defgeneric printVal (stream val)
    (:documentation "printVal prints val to the stream"))

;; simple-array is Common Lisp's version of slice
(defmethod printVal (stream (val simple-array))
    (write val :stream stream))
;; for the string counterpart
(defmethod printVal (stream (val string))
    (write-string val :stream stream))

It is not like Go doesn’t support anything close to this, it does. It supports interfaces, which are abstract data structures that only contain function signatures in them, so that you can essentially plug and play for any custom data structure you created.

What fails me in Go’s version of abstraction is that it cannot be applied to normal types, I cannot add to any outside package’s types, only my own. Perhaps, then, it would create three other problems:

  1. debugging would be harder
  2. how can we call the underlying function if we need to
  3. when does modifying outside packages stop?

The way I deal with it in Go is simply to use different names. Sure, it’s more verbose and is overall unelegant but it’s what works™. Apparently, it’s not only me, as in Go’s strconv standard package, there is the need for parsing and format different data types. Take a look at these function signatures

// from strconv
type ParseBool func(str string) (bool, error)
type ParseComplex func(s string, bitSize int) (complex128, error)
type ParseFloat func(s string, bitSize int) (float64, error)
type ParseInt func(s string, base int, bitSize int) (i int64, err error)
type ParseUint func(s string, base int, bitSize int) (uint64, error)

no generic slice datatype

This means that you cannot create a function that modifies a slice generically which sometimes is needed. Think about the filter or map function in Common Lisp and how useful it’d be in Go.

Basically, a filter function filters out any item that doesn’t return true from the callback. A simple, though non-working, Go example would be the following:

func filter(arr []interface{}, fn func(v interface{}) bool) {
    ret := []interface{}
    for _, v := range arr {
        if fn(v) {
        ret = append(ret, v)
        }
    }
    
    return ret
}

Then, you could use something like:

// pseudo code
func filterNegativeNumbers(arr []int) []int {
    return filter(arr, func(v int) { return v >= 0 })
}

That is extremely simple and elegant but if you wanted to accomplish this in Go, you’d have to convert between slice of interface and slice of integers. The following would be a working Go solution:

func filterNegativeNumbers(arr []int) []int {
    inter := []interface{}
    for _, v := range arr {
        inter = append(inter, v)
    }
    
    inter = filter(ret, func(v interface{}) bool {
        num, ok := v.(int)
        if ok && num > 0 {
            return true
        }
        
        return false
    })
    
    ret := []int{}
    for _, v := range inter {
        ret = append(ret, int(v))
    }_
    
    return ret
}

Also, it’s not possible to have nice things like this in Go:

// remove the last element from a slice
func allButLast(s []interface) []interface { return s[:len(s-1)] }

Instead, you have to do this:

func stringAllButLast(s []string) []string { return s[:len(s-1)] }
func intAllButLast(s []int)       []int    { return s[:len(s-1)] }
func boolAllButLast(s []bool)     []bool   { return s[:len(s-1)] }
// and so on...

The way some other languages do it, for example Common Lisp, is through a generic list or slice data type. Take a look at this:

;; common lisp's version of allButLast
(defun allButLast (lst)
    ;; common lisp is dynamic
    ;; but you can assert types
    ;; at compile-time
    (declare (list lst))
    (subseq lst 0 (- (length lst) 1)))

Perhaps, simple functionality like this makes the Go compiler a lot more complicated. It’s not only the Go compiler, however, it’s also a host of other tools like the go-lint, gofmt and so on. Also, how would older versions of the languages deal with this new addition? Such a simple and needed change isn’t free.

data structure inheritance

This is the most ridiculous one so far. So far, I didn’t have the chance to have to use this feature in ft but I can see it coming some day. Data structures cannot be inherited directly, so you have to re-create methods and pass them to the parent data structure or expose the child structure to outside packages.

Solution 1 is re-create the method(s):

type Dog struct {
    name string
}

func (d Dog) Name() string { return d.name }

type Animal struct {
    d *Dog
}

func (a Animal) Name() string { return "Animals is: "+ a.d.Name() }

Solution 2 is to expose the child structure to the outside packages

type Dog struct {
    name string
}

func (d Dog) Name() string { return d.name }

type Animal struct {
    Dog *Dog
}

// Then call it like so
// animalInstance.Dog.Name()

One note about this is that it is far better to have an interface as a child and a structure that modifies that interface to produce the wanted result. The following code snippet is a more clean and concise solution:

type Animal interface {
    Say() string
}

type Dog struct {}
func (d Dog) Say() string { return "Bark!" }

type AnimalSays struct { a Animal }
func (a AnimalSays) Say() string { return "Animal Say: " + a.a.Say() }

wielding Go to your power

Interfaces aren’t used enough in my opinion. When in doubt, use an interface and implement it later because interfaces are testable.

One thing I used in ft is defining a controller that can be supplemented by definition protocols. Take a look at the underlying interface:

type ControllerModel struct {
    ID     string
    Reason string
}

type Controller interface {
    // Error is an error method, it is used whenever an error occurs.
    Error(c ControllerModel)
    // Value is a success method, it is used whenever a successful operation
    // happens
    Value(val interface{}) error
}

Then, I implemented a DummyController so I could test any function with a Controller parameter. This also allows for plugging different protocols for more accessibility. an NFS server, an HTTP server, you name it, it could be implemented with little to no code modification.

optional interface fields

This is taken from a different closed-source project (at-least for now) because you cannot wield an interface into a child interface. So, this wouldn’t work:

func returnWriterAt(w io.Writer) io.WriterAt {
    at, ok := w.(io.WriterAt)
    if !ok { return nil }
    
    return at
}

But, this works:

type ImplementsWriterAt interface {
    HasWriterAt() bool
    WriterAt()    io.WriterAt
    Write([]byte) (int, error)
}

type writerAt struct {
    at io.WriterAt
    wr io.Writer
}

func (i *ImplementsWriterAt) HasWriterAt()    bool         { return i.at != nil    }
func (i *ImplementsWriterAt) WriterAt()       io.WriterAt  { return i.at           }
func (i *ImplementsWriterAt) Writer(b []byte) (int, error) { return io.wr.Write(b) }

func NewWriterAt(wr io.WriterAt) ImplementsWriterAt {
    return &writerAt{wr}
}

Or, you could make optional fields return the value you want with an error if it’s not implemented.

type Number interface {
    Integer() int
    Float() (float64, error)
}

conclusion

While Go can be improved tremendously both syntaxically, it still isn’t too bad for a server backend language, sure it trades power for performance and safety but that isn’t a bad trade for a project like ft.