Following the famous The Linux Programming Interface book from Michael Kerrisk. But now in Golang (instead of C).
I started this project to learn:
- more details about how Linux works
- Golang
- something about System Programming (coming from a scripting language background)
- understand C idioms and acronyms by working through the book (C has a simple syntax, but many C programs are hard to understand without some practice in reading/writing C code)
I choosed Golang as:
- it is easier to understand
- has less gotchas than C (and thus is more practical to program)
- it is the language of Kubernetes and DevOps (both matter to me in my profession)
- it probably is also easier to start into system programming than with Rust
If you prefer videos: I’m putting content about this project on my youtube channel
I follow the structure of the Distribution source C source code.
All code examples are formatted with gofmt
and linted with golangci-lint –enable-all.
I try to organize the Golang code as described in https://github.com/golang-standards/project-layout.
Interesting, but nothing to code.
Important, but still nothing to code.
Here, Michael Kerrisk starts coding and writes an own error handling module. The concepts aren’t really implementable the same way in Golang (unless we want to use “unsafe” constructs).
We find in the C source code 3 ways how programs could be terminated and several
error message functions. None of them really applies to the way Golang handles
termination and errors, so I only implemented a Terminate function that can be
called with go run ./cmd/errors/terminate.go
but it’s not intended to be
really used.
If an environment variable EF_DUMPCORE
is set, the C programs are supposed to
run C’s abort()
method what in the end raises an ABORT
signal event what
results in abrupt termination of the program by the system and writing a core
dump.
In principle we could implement it as well in Golang, but if I understand it
correctly it is highly recommended not to abort a running Golang program. If
we want a core dump for what ever reason of a Go program, we only need to set
ulimit -c unlimited
(or set a number for how big the core dump should be max)
and set environment variable GOTRACEBACK=trash
to get a core dump when
exiting.
The C function _exit()
stops the program without any flushing of file handlers
or running atexit()
methods. Even in C world, usually that’s not the way to
stop a program. It is intended to be used with for example forks where we don’t
want to flush or close handlers shared.
We can implement this functionality in Golang with calling os.Exit(EXIT_CODE)
.
But we should not need it in Golang as we don’t need forks for parallelity as
Golang has goroutines instead.
What we might need from (e.g. when argument parsing fails) is writing a message
and stop right there. We can use log.Fatal(msg)
to achieve it, but even this
is discussed whether it is code smell or not in Golang.
In C we’d call exit()
(now without the underscore) to achieve it. Michael
Kerrisk implements several functions to also dump a human readable error message
along for the errno
or to write an own error message or to write a usage
message if argument parsing fails.
First of all, we have a different error approach in Golang. Instead of one
global errno
variable that can be affected everywhere in the program (that’s
why it’s copied in the C sources), Golang returns an error variable for every
call that could fail. In case, this error variable knows already its human
readable error message by err.Error()
. Indeed, there is no way and no need in
Golang to lookup the corresponding error message for an errno
. (Unless we want
to import <errno.h>
from C). So, the usual way in Golang we communicate an
error to the program user is to call panic(fmt.Sprintf("Something failed: %s",
err.Error()))
and in addition to what C makes we also get a minimal stack trace.
For argument parsing, I’ll use most of the time argument parsing modules, so no
need to implement errUsage
methods here. This is debatable as there is some
overhead with them (e.g. they might malloc
memory), so in some system
programming situations, this might not be the preferred way (e.g. to write a
tool to inspect out of memory situations).
Here, first some basic fileio is implemented (opening, closing, writing, reading) in ./pkg/fileio/fileio.go
Implemented:
- copy functionality that can be called with
go run cmd/fileio/copy/copy.go <SRC> <DEST>
- seek program to write, read text and hex from a file and seek to a random position
- tee functionality that can be called with
go run cmd/fileio/tee/tee.go --append <DEST>
- sparse copy functionality to copy files with holes that is active if set a
--hole-size <LENGTH>
option to the copy program
You can find a video about my implementation and discussion of Chapter 3 and 4.