Category
As mentioned in my last entry, I've recently become curious about the Go programming language, so I took a course to learn the language, partially to support add-ons for Hugo and partially to compare with other languages I've used--most notably, Delphi. What I found was a simple but powerful language, thoughtfully designed, that promotes good coding style and breaks some established patterns of thought I've had with object-oriented programming.
One of the big advantages of Go over other popular languages is that, like C++ and Delphi, it compiles to an executable binary instead of being scripted like PHP, Python, or Ruby. This is a proudly touted feature that also helps promote Hugo.
The syntax, at first glance, appears to mimic C or JavaScript but there are a couple of surprising similarities with Delphi. Furthermore, assumptions made by the compiler that encourage you to simplify your code, make it resemble Python in a way.
In this blog, I'll give a brief overview of what I learned about the Go language from the perspective of a programmer who's used mostly Delphi (Pascal) but with a sprinkling of other languages (C, C++, C#, VB, and PHP) throughout his career.
First, a disclaimer: I'm very new to the Go programming language and some of what I describe may not be completely accurate--it's based on what I understood as I went through the introductory course I just completed and the simple programs I wrote. There are likely exceptions to what I describe or the generalities expressed may not be as prevalent in the industry as I suspect. Also, my concentration was simply the language and writing simple console apps--I have not yet been exposed to the wide array of packages in the Go ecosystem or done any Windows or cross-platform exploration with the language.
Packages
A file of Go code (a module
) starts with the keyword package
and a name. All .go
files in the same directory typically share the same package name. To use functions and types from an external package of library code, you import
it (like the uses
clause in Delphi). The .go
files in the same folder do not have to be explicitly imported--they're automatically included in the package.
Go is a case-sensitive language. This is especially important when naming functions and identifiers at the package level. In Delphi, things that need to be accessed outside of a unit are placed in the interface
section and the hidden details are in the implementation
section. In Go, you simply capitalize identifiers (proper-cased, not all caps) to make them visible outside of the package; because of this, package-level identifiers are lower-case (or at least start with a lower-case letter) unless you need them available from elsewhere.
Functions
Functions start with the keyword func
followed by the function name and parenthesis with optional parameters inside the parenthesis followed by an optional return type and then the block of code that makes up the function. Blocks are delineated in Delphi with begin
and end
but use curly braces in Go--just like C# and Java and several other languages. When a Go program starts, it always looks for and runs the main
function:
func main() { }
If the function does not return a value, it's return type is simply left off as in the main
function depicted above; this is like a void
function in C and the procedure
type in Delphi. Unlike other any other language that I know, Go functions can have multiple return values. A typical use for this is to return an error along with the result; for example, the Atoi()
function from the strconv
standard Go package, converts a string to an integer, returning both the integer (if successful) and the possible error:
n, err := strconv.Atoi(s)
You can then test err
and if it's nil
, proceed to use the successfully converted integer, n
. In my opinion, this is much nicer than wrapping this in a try-except block in Delphi or returning the error in a var
parameter. Speaking of parameters, there are no var
parameters in Go; all parameters in Go are passed by reference; however, you can also pass pointers and the functions can modify the value at which they point.
A function with two parameters and one return value could be declared like this:
func DoSomething(n int, s string) bool { // }
To declare two return values, wrap them in parenthesis:
func ConvertValue(v string) (n int, ok error) { // }
By the way, error
is a built-in "interface" type and has an Error() string
function. I'll talk about interfaces below.
Semicolons
You may have noticed the sample Go code here does not have any semicolons at the ends of the lines. While the formal Go grammer does require semicolons to terminate statements, an interesting aspect of the Go lexer is that it automatically inserts them as it scans the code based on the context of the line. You can have them in your code but they're not necessary in most cases--and therefore, discouraged.
Variables
If you saw the variable assignment above with the :=
and caught your breath exclaiming, "That's like Delphi!" then I'll pause ... and let you revel in this short moment because the :=
has a very special use case--and it's not used as often as you might hope.
Go, like Delphi, is a type-strict language; each variable has a specific type established when it is declared. There are two ways to declare variables. The first is somewhat similar to Delphi because it uses the var
keyword (but doesn't use a colon between identifiers and their type); there isn't a var
declaration section, each variable is defined on its own line whenever you need one:
var n int var s string
Later, you can assign values to them with a single =
sign:
n = 1 s = "hello"
The second way to declare a variable is by initializing it with a value from which it gleans the type and declares it implicitly. This is the special case (and the only time) where the :=
is used in Go:
n := 1 s := "hello"
In this case, there's no need for the var
keyword or explicitly declaring the variable types as the type is inferred with the value assignment; in other words, n
is implicitly declared as an int
and s
as a string
. The :=
can only be used once on an identifier within a scope; after declaring and initializing a variable, further assignments can only be done with the single =
operator.
One other note is that declaring a variable without an immediate assignment of your value always initializes the variable to the "zero-value" for its type; numeric variables are set to zero, string variables to an empty string, pointers to nil, and Booleans to false; this is also true with arrays where each element of the array is initialized to the zero-value for its element type. This is nice to count on rather than be subject to the fate of whatever value happens to be in the memory location for a new variable--I've seen many hard-to-find bugs because of uninitialized variables (which, of course, a good programmer would never have!).
Types
Speaking of variable types, here are the basic variable types in Go:
bool
- (true/false)byte
- (alias for uint8)rune
- (alias for int32)int
- (many variations--see below)float32/float64
complex64/complex128
string
struct
interface
Like Delphi, there are many variations of integers depending on the size of number you need to deal with and the platform you're on: int8 (-128..127), uint8 (0..255), int16, uint16, int32, uint32, int64, and uint64; the standard int
type is either int32 or int64 depending on the platform.
Strings are immutable and 0-indexed. For example, in s := "Hello"
, the first character is referenced with s[0]
and the last one with s[len(s)-1]
.
Each of the basic types can be used in a arrays, slices, or maps. You've likely heard of arrays but what are slices or maps? A slice
is simply a portion of an array
and, in fact, always has a "backing array" to which it points, either created implicitly (e.g. with the built-in append()
function) or assigned to specific elements of an array explicitly. Arrays always have a fixed length, slices can grow or shrink. A map
is like the TDictionary
type in Delphi and can be used to create an index of any type to a value of any other type. Here are some examples of these types:
// declare an array of 10 numbers var nums [10]int // declare a slice of integers; then append the first five elements (0 to 6-1) from the nums array var nums5 []int nums5 = nums[0:6] // declare a map of three-letter acronyms, initialized with 3 entries dict := map[string]string { "TLA": "Three-Letter Acronym", "API": "Application Programming Interface", "AFK": "Away From Keyboard", }
Slices are more powerful and flexible than you might at first think--and the topic gets deep quickly.
A struct
is like the record
type in Delphi: it is a user-defined type containing fields. However, structs cannot contain functions; instead, they are associated with a struct type in the function's declaration like this:
type person struct { fname string lname string age int } func (p person) fullname() string { return p.fname + " " + p.lname }
The "fullname" function is associated with the "person" type and contains an implicit parameter of type "person" that can be used within the function. When considering Delphi's class or record types, this seems rather loosely connected--but it works and it's simple. (Simplicity is an overarching goal of the Go language.)
An interface
in Go is very similar to Delphi's interface
type in that it is an abstract type that declares a list of function names that must be implemented but has no implementation itself.
For example, the following establishes a "printer" type listing one function, print()
, which must be implemented and associated with a struct for the struct to be considered as adhering to the set of "printer" types:
type printer interface { print() }
Let's say we have a store with books and games. We could create a couple of different types and as long as each implements print()
, then they will both implicitly match the printer
interface:
type book struct { title string author string price float } func (b book) print() { fmt.Printf("%s, by %s - %.2f\n", b.title, b.author, b.price) } type game struct { title string category string price float } func (g game) print() { fmt.Printf("%s [%s] - %.2f\n", g.title, g.category, g.price) }
Now, if you had an array of printer
interfaces loaded with various book
and game
items, you could call the print()
function and it would call the appropriate one for the element's type:
var inventory [100]printer // ... inventory[i].print()
I found it interesting that you don't actually declare the book
or game
types as being of the printer
interface, just the fact the associated function print()
exists is all it takes to qualify. It's design provides for simple dependency injection.
A couple of important aspects of the struct
type I should mention before moving on is that structs cannot inherit from other structs but struct
fields can be of any type, including another defined struct
type; a struct with a field of another struct is called "embedding" and there are some shortcuts for accessing fields that make working with them simple--as you would expect with Go.
Here's an example of a struct
type with an embedded struct field referencing the "person" struct type I showed earlier:
type employee struct { p person title string salary float }
With this, you can reference the fname
and lname
fields (defined by the p person
field) as if they were native to the employee
struct.
There is no "class" type in Go; the struct
with embedded sub-types and associated functions and its use of an interface
to identify related functions as depicted above are what Go considers all you need to support object-oriented programming. But Go doesn't really try to classify itself as a standard OOP language; indeed, it's FAQ says this, "Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general." While going through the Go course, I kept looking for stronger implementations of how I learned object-oriented programming but as I read articles about why the language was designed like it is, I started to think that maybe all the OOP syntax I've come to expect and work with is more of a restriction than protection. Like I said earlier, it works and it's simple.
Notice the lack of a "date" or "time" type? A time
struct is defined in the standard "time" package--it's not a native type.
Flow Control
The if
control structure is nearly identical to the C/C++/C# way of doing things. Its initialization clause can declare and initialize a variable:
if x := f(); x < y { return x } else if x > z { return z } else { return y }
The switch
statement is also similar to the C class of languages, but with a couple of simplifying enhancements. The Boolean expression part of the syntax can be preceded by an assignment:
switch s := getinput(); s { case "one": return 1 case "two": return 2 default: return 0 }
The expression can also be left out completely which means the case
statements need full Boolean expressions of their own:
switch { case x < y: return "less" case x > y: return "more" case x == y: return "equal" }
Switch statements in Go can also test a variable's type:
switch x.(type) { case int32, int64: return "integer" case bool: return "boolean" case string: return "string" }
Notice no break
statements? In Go, each case
has an implicit "break" at the end before the next case
. However, you can override that with the fallthrough
statement which resumes control with the next case
:
switch num := number(); { case num < 50: fmt.Printf("%d is less than 50\n", num) fallthrough case num < 100: fmt.Printf("%d is less than 100\n", num) fallthrough case num < 200: fmt.Printf("%d is less than 200", num) default: fmt.Printf("%d is out of range", num) }
Personally, I find this a much smarter approach than C's break
as it's far more common to handle each case separately; fallthrough
is most assuredly the rarer case, thus Go's switch syntax is cleaner.
The only looping construct in Go is the for statement--there is no repeat-until, while-do, or do-while; the flexibility of Go's for
allows you to do it all with this one statement and its variations.
We'll start with the standard C-like syntax that initializes a variable and repeats the block of code until a condition is false and incrementing at the end of each loop:
for i := 1; i < 10; i++ { // }
To implement a "while" loop, provide only a conditional:
for getDigit() != 0 { // }
You can even write an infinite loop, assuming somewhere in the block is a break
or return
statement:
for { // break }
A Complete Program
Those are the basics. Before I close, I'll share a complete Go console program, explanation will follow:
package main import ( "bufio" "fmt" "os" ) func main() { if len(os.Args) != 2 { fmt.Println("Usage: go run readfile.go <filename>") os.Exit(1) } filename := os.Args[1] file, err := os.Open(filename) if err != nil { fmt.Println("Error opening file:", err) os.Exit(1) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { fmt.Println("Error reading file:", err) os.Exit(1) } }
In this example, the program first checks if the number of command-line arguments is exactly 2 (the program name and the filename). If not, it prints an error message and exits with a status code of 1.
Then, it opens the file specified by the filename passed on the command-line, reads the lines in the file and prints each line; if there's an error reading the file, it prints an error message and exits with a status code of 1.
Finally, the file is closed using defer
so that it will be closed even if there's an error or panic in the program.
Summary
There are many other interesting tidbits about the language, such as "named" versus "positional" parameters, the defer keyword, using range
to iterate over a collection of items such as an array or slice, and how to recover from a call to panic. My introduction to the language was a fun learning experience. I'm impressed with this language but not sure yet how much I'll do with it as Delphi is my bread-and-butter. As stated earlier, I do intend to use it in conjunction with migrating Drupal sites to Hugo using open-source migration tools written in Go but I still don't know how to access databases or build a GUI application. I have a ways to go and not as much spare time as I'd like.
Add new comment