Skip to content

Instantly share code, notes, and snippets.

@jandre
Last active January 1, 2016 23:19
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jandre/8216047 to your computer and use it in GitHub Desktop.
Save jandre/8216047 to your computer and use it in GitHub Desktop.
One Day Of Go

One day of Go

One of my ongoing resolutions is to learn a new programming language every year. I've been looking at Go for a while and I thought it would be fun to use the 'holiday' today to really get cranking. I recorded some random impressions as I was chugging along...

When learning a language, I'm one of those people that can't simply read documentation and contrived code examples with any usable level of recall -- I need to internalize it by writing code to solve a legitimate problem. So, I decided to write a little library, a parser for the Linux /proc filesystem. There aren't very many good ones that I've found for Go (and I am thinking of some projects I may want to use it for in the future).

Steps taken:

  1. Read a bit of code

  2. Setup my working environment

  3. Write some code

Read a bit of code

I skimmed through the Introduction to Go, did a few of the Go tour exercise, and read through the examples from this useful blog post, "Code to Read When Learning Go". Given my learning style, the code examples were for me, by far, the most helpful -- I referenced the examples frequently as I was working to see how things were designed and written.

First impressions: Go code is kinda ugly to me?

My first raw impression of Go is that Go code is kinda ugly. Does that make me superficial? ;) I don't know. I don't like the conventions. I don't much care for Camel Casing, and I don't like everything being capitalized. I especially don't much care for the encouraged use of one letter variables.

Here's a bit of code from net/http/request to show what I'm talkin' bout:

func (l *maxBytesReader) Read(p []byte) (n int, err error) {
  if l.n <= 0 {
    if !l.stopped {
      l.stopped = true
        if res, ok := l.w.(*response); ok {
          res.requestTooLarge()
        }
    }
    return 0, errors.New("http: request body too large")
  }
  if int64(len(p)) > l.n {
    p = p[:l.n]
  }
  n, err = l.r.Read(p)
    l.n -= int64(n)
    return
}

Is that really preferable to something like this?

func (byteReader *maxBytesReader) Read(buf []byte) (bytesRead int, err error) {
  if byteReader.remaining <= 0 {
    if !byteReader.stopped {
      byteReader.stopped = true
        if res, ok := byteReader.write.(*response); ok {
          res.requestTooLarge()
        }
    }
    return 0, errors.New("http: request body too large")
  }
  if int64(len(buf)) > byteReader.remaining {
    buf = buf[:byteReader.remaining]
  }
  bytesRead, err = byteReader.input.Read(buf)
    byteReader.remaining -= int64(bytesRead)
    return
}

To each his own, I suppose... Plus, I expect I'll get used to it over time.

Setting up my working environment

I used Ubuntu for my dev environment, since I needed Linux for my library. There was no ppa for Go1.2, so I had to install Go from gvm. Fairly simple.

For my editor, I installed the vim scripts that come with Go. I remap'd ctrl-f in vim to run gofmt, a tool that auto-formats your Go code for you, another artifact packaged with the Go distribution. Can't complain about a lot of the tooling, at least if you are a vim user, Go has you all set ;)

Workspace hell

From there, I started to setup a repo for my project. Initially, I setup a repo with the workspace structure described here with subdirs ./src,./bin,./pkg, before I realized that was terribly wrong. I realized all of the Go libraries I was reading had everything in a flat structure. I then understood that the workspace was a local thing, and ./src was to house many repos, so I created a ./procfs dir in ./src and initialized my git repo in that dir accordingly.

This clarified another confusing thing for me: GOPATH. When you use the Go tools, you need to set an environment variable GOPATH. It's not set to the particular package you are working on, it's set to the workspace that contains the ./src,./bin,./pkg structure. It uses this as the root to dump all of the build files and other sources you fetch in those corresponding subdirectories. It made sense after a while, but setting the GOPATH env variable is still kind of annoying to me. Why can't GOPATH just be set to my cwd by default? And overwritten with the env variable if it exists?

Write a bit of code

With my workspace ready, I was ready for coding! I decided I wanted to base my api design similar to ocaml-core's Procfs module, which I found quite nice. Since the design was fairly straightforward (and dictated by the /proc structure), it was just a matter of plug and play.

First interactions with C are nice

Interfacing with basic libc functions was relatively easy. To use sysconf(3), I imported a "C" pseudo-library with a comment referencing my #include <unistd.h> statements, and calling sysconf() was straightforward from there. However, documentation for 'goc' seems to be pretty sparse.

At one point, I was trying to use the output of sysconf, which returned a _Ctype_long, and multiply it with a Go int64. Naturally, it complained about a mismatched type, and figuring out how to do the conversion was not straightforward because of the limited documentation (I initially assumed _Ctype_long may have been an "object" and I was looking for conversion methods on it).

You still have to think about pointers

Someone described to me that Go was an 'easier C' because you don't have to think about pointers. While memory management certainly is convenient, this isn't really true. You still have to think about pointers, and when to use a pointer vs 'stack' allocations (to avoid memory copies for performance reasons, for example) and design appropriately. Just today, I made a couple of mistakes that resulted in runtime null dereferences doing stupid things.

Thank god for strings

What I will say is that Go is an easier C (times 1000) in terms of string handling. For better or worse, strings are the workhorse type for most programs. To be able to do ``"a" + "b"` is so refreshing. You have no idea.

Dependency version management is scary to me:

Let's say my little Go library uses some external libraries, 'github.com/blah/foo'. Someone uses my library, and does a go get to fetch all of the dependencies recursively. How do they know what version my library is depending on? It will always get 'master' currently, which is scary to me. I think the solution here is to somehow include the full src of the dep github.com/blah/foo in my repo, but I'm not really sure how to organize that at the moment.

I feel like SOME kind of standardized specification would be helpful to document what tag/version of an external dependency is needed, even if it isn't enforced (e.g. a gem spec or node package.json). Maybe you could event specify it right in the import statement, e.g.:

  import ( 
    "github.com/blah/foo?tag=1.0.1"
  )

Imports need polishing

In fact whole import system is confusing. I created several local 'packages' under my procfs package, e.g.: procfs/stat, and tried to include("procfs/stat"). Well, this worked great until I checked in the repo and did go get github.com/jandre/procfs, then nothing worked. Googling around, I learned that there was no way to reference a relative package; everything has to be done from ./src. Ugh. So I changed all of my imports to "github.com/jandre/procfs/xxx" adjusted my workspace structure accordingly, and everything was golden. I was only mildly consoled by the fact I wasn't the only person who found this non-intuitive. It either should never have accepted a relative path in the first place (errored on compile), or just allow me to import directories below my package path in a relative way.

Go is surprisingly sparse

Go is surprisingly simple and sparse. Not a lot of synatic sugar and features to get super excited about (I would love to see pattern matching and more powerful type inference). On the other hand, this isn't necessarily a bad thing. I do rather appreciate that they seem to be carefully thinking about language features vs throwing in the kitchen sink.

Goroutines and channels are a nice alternative to the whole coordinator <-> queues <-> worker model:

I feel like I've written some variant of the whole "requesting thread sends data to a threadsafe queue, another pool of threads or workers pops the data off the queue and does some work on it, pushes results to queue and notifies" a thousand different times. Goroutines and channels allow you to do this in a quick and painless way.

Why this vs the popular event loop, you say?

I have one word for you: Bounds. The bounded 'queue' or channel model delightfully allows me to control my resource consumption by blocking upstream when the downstream workers run out of resources. Think of your async worker code as an assembly line, and the event loop callback processor as your worker. Upstream, some dude is queueing up all of these http requests, for your worker guy (the event loop) to well, handle.

If the workers are too slow, what happens? Well, the requests pile up. In an event loop, it will be unbounded -- granted, the requests may be much smaller than say, thread overhead, but they are taking up memory somewhere. Futhermore, you generally have no visibility into the fact this is happening, which is scary, and limited ability to throttle it the way most of these async frameworks are designed. This gets even more complex as tasks flow downstream from a -> b -> ... -> n.

In contrast, using a bounded channel or queue, I know if my upstream task is pumping data into it before the workers can get to it, and I can specify it to block until the workers 'catch up', avoiding consumption of all of my memory. This follows gracefully upstream, even if I have data flowing from a -> b -> c, and throttling becomes much easier to reason about. This is what I want.

Honestly, these are the more exciting features of the language (and the most compelling argument to choose it over say, C, C++, or even the JVM) and looking forward to exploring it more.

Test infrastructure: Basic but quite usable

Writing tests is easy. Just create a suffixed file with test, e.g. module_test.go, and you can run your tests with go test -v path/to/module. Any function you prefix with Test gets turned into a test. No surprises there. I like how simple it was because I'm lazy, and sometimes a lot of these test frameworks have elaborate DSLs that are time-consuming to learn. Here it was literally like just "write normal Go code, and emit t.Error() or t.Fatal() if your test fails". Boom.

In conclusion

I'd say my first 24 hours with Go were a success. Despite a few hurdles, I found the experience programming in Go enjoyable, and I'm definitely interested in seeing how Go will evolve. I'll likely finish up the procfs library in my spare time, and there are still some basic 'infrastructure' things I need to explore (E.g., logging, debugging), but I'm going to try to build something more substantial in the near future. Stay tuned for updates!

@eblume
Copy link

eblume commented Jan 2, 2014

I wanted to remark that I agree with you on all counts. I think you will find, as have I in my admittedly short time working in Go, that the GOPATH workspace paradigm starts out feeling obtuse and strange but ends up being a very sane alternative to the same issue in other languages. Contrast it with .node_module folders existing all over the place or Python's umpteen different install locations depending on user permissions, python version, packaging tool, etc. In Go, everything winds up in GOPATH, and it's all per-user and statically linked. Very simple, just also very different.

Disclaimer: I am still a Go newcomer so I may be wrong about some of this.

@rogpeppe
Copy link

rogpeppe commented Jan 2, 2014

Nice stuff! I definitely might use this some day.

A few little remarks based on a very brief look at the code
(well, somewhat less brief as I got into it :-])

  • I'd probably merge all those sub-packages into the main package.
    They're closely related enough that I don't think they benefit much
    from the separation. That means the utils package could be merged
    too.
  • doc comments should be whole sentences. http://golang.org/doc/effective_go.html#commentary
  • in limits.go, you could probably use strings.Fields instead of splitBy2Whitespace
  • Your makeUnit function could almost be "return Unit(str)". I'd be tempted to make
    things a little more data driven, e.g. http://play.golang.org/p/VV47oCbnIr
  • nice use of reflection in limits.go, but tbh I'm not sure it's that much of a win over
    something like this, since all the entries are of the same type: http://play.golang.org/p/qq8-AohdFl
    The same might well apply to MemInfo, although it's perhaps less clear cut there,
    as I suppose someone might put a non-int type in there some day.
  • reflection is definitely a win in structparser.go though, except something like this is
    a bit more direct - you can test the types directly: http://play.golang.org/p/MLIxbuGJUw
  • I think you could lose EpochTimestamp entirely - better, I think, to convert
    to time.Time immediately, so the user never has to know any of the juggling going on underneath.
    (also, presumably the times may become wrong if the clock tick rate changes after a read)
    Something like this, perhaps: http://play.golang.org/p/z_Q-EJigT4

Hope this is at least slightly helpful!

@tompao
Copy link

tompao commented Jan 2, 2014

Nice write-up, thanks.

P.S. it's "type inference"

@egonelbre
Copy link

Go code is kinda ugly to me? - yup, to me that part could use some cleanup. You could ask on golang-nuts whether they are interested in improvements to that part. They accept readability improvements as well, if it fits with the rest of the code; but the core team deals more with the runtime. Not all of software can be well-designed and some parts get overlooked or are good enough.

Dependency version management - The only way to ensure that your thing always compiles is to bundle your program with its dependencies. Any other method will have a corner case where it won't work (e.g. the repository gets deleted). Also there's godep, and other, if you accept the trade-offs.

Imports need polishing - your suggested alternative breaks when trying to use "go get" dependencies... how would the tool know where from "jandre/procfs" should be downloaded, github, bitbucket... or some obscure server. Also, relative imports do work, but have to be prefixed "./" or "../"; and most of the time shouldn't be used, since the go get won't work that nicely with them. Some, projects do use custom project layout (e.g. camlistore) or package the full GOPATH (e.g. spexs); but it only works nicely, if you provide your own downloadable binaries or build scripts.

@matrixik
Copy link

matrixik commented Jan 3, 2014

Check also go-wiki for some useful tips: https://code.google.com/p/go-wiki/w/list
like table driven tests: https://code.google.com/p/go-wiki/wiki/TableDrivenTests

You can check nice source linting tool: https://github.com/golang/lint
You can even check your code online: http://go-lint.appspot.com/github.com/jandre/procfs

@jandre
Copy link
Author

jandre commented Jan 4, 2014

@rogpeppe thanks VERY much for the feedback! I've started implementing some of it today!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment