Go Structs
Background
I have been playing around with Go Lang recently and have been really enjoying it. Go feels like a “modernized C”, still minimal like C, but with modern programming langauge features to make it easier to use, especially after using more current languages.
One thing I learned from a friend is how to use: “Go Structs” to prototype functions and create function instances. Looking up structs, they kind of look like C structs to me, a kind of customized array.
He was so nice and wrote out a simple version of a program using Go Structs. I tried to look for more info, but couldn’t find it… until now.
Go Lang book describes these as methods, which looks like the official name for this is method.
After using them, I can see why my friend told me to: “Use Go Structs”.
Go Structs Methods feel like a prototype, it describes the function, and instance is the use of the prototype.
They are useful as they:
- they isolate naming of functions
- methods can get values from the struct
- values on the struct are settable by methods
Motivation
I want to document some of my learnings in Go, especially around Go Structs. I couldn’t find much in terms of documentation, and if my friend didn’t write a bunch of Go code for me showing how he uses Go Structs, I would still write C styled Go code, which isn’t so pretty in Go.
Requirements
If you want to follow along, these are instructions to setup a vagrant enironment using virualbox.
Setting up environment
- Install vagrant
- clone repo:
$ git clone https://github.com/a-leung/go_structs.git
In terminal, run these commands:
$ cd go_structs
$ vagrant up
Test
To run the automated tests, run these commands in a terminal:
$ cd go_structs
$ vagrant up
$ vagrant ssh
...
$ go test -v go_structs
Normal Functions
As a demonstration, the following is a normal function in Go:
func Hello() string {
return "Hello"
}To use this, call the function like any other function, in the main program file:
// hello.go
package main
import "fmt"
func Hello() string {
return "Hello"
}
func main() {
fmt.Println(Hello())
}It’s pretty straight forward. Calling the Hello() function is
possible anywhere, from any function or method.
Test
To test the code in the article, I am using the following basic test
file, even though it does not test the functions directly. I want to
just run: $ go test hello
// hello_test.go
package main
import "testing"
func TestHello(t *testing.T) {
if true == false {
t.Error("true is not false?!")
}
}Go Method or “Go Struct”
Now with Go Methods, to define AnotherHello() method:
func (s *Struct) AnotherHello() string {
return "Another Hello"
}But, trying to call the method will result in an error:
// hello.go
package main
import "fmt"
func Hello() string {
return "Hello"
}
func main() {
fmt.Println(Hello())
fmt.Println(AnotherHello())
}go test hello
# hello
go/src/hello/another2.go:3: undefined: Struct
go/src/hello/hello.go:13: undefined: AnotherHello
FAIL hello [build failed]Looks like just adding the (s *Struct) prevents any call to it that
is not associated with the Struct type.
Let’s fix that by adding Struct type definition:
package main
type Struct struct {}
func (s *Struct) AnotherHello() string {
return "Another Hello"
}go test hello
# hello
./hello.go:13: undefined: AnotherHello
FAIL hello [build failed]Ok, that still fails. Well, looks like the way to call AnotherHello
is to instantiate the struct:
package main
import "fmt"
func hello() string {
return "Hello"
}
func main() {
fmt.Println(hello())
another := &Struct{}
fmt.Println(another.AnotherHello())
}go test hello
ok hello 0.002sSo, without the line: another := &Struct{}, AnotherHello() is not
called. To call the function to, prefix with the variable
instantiating, which in this case is: another.
If the instantiation was: superStruct := &Struct{}, to call
AnotherHello() would be: superStruct.AnotherHello().
Get Values from Struct from Method
As this is a struct, they can store anything. So the method can even access items inside. Let’s say the struct & method is setup as:
type Greeter struct {
string GreetingMessage
}
func (g *Greeter) SayHello() string {
return g.GreetingMessage
}Now, wherever initializing the struct, it will set values inside the struct:
// greeting.go
package main
type Greeter struct {
GreetingMessage string
}
func (g *Greeter) SayHello() string {
return g.GreetingMessage
}
// greeting_test.go
package main
import "testing"
func TestGreeting(t *testing.T) {
english := &Greeter {
GreetingMessage: "Good day",
}
if (english.SayHello() != "Good day") {
t.Error("not getting value from struct")
}
french := &Greeter {
GreetingMessage: "Bon Jour",
}
if (french.SayHello() == "Good day") {
t.Error("not getting value from struct")
}
}So, one can have a prototype method which can be purely implementation and only upon usage, set the values.
In this case, one instance for French, another for English, another
for Japanese, they all use the same prototype method: SayHello()
from the Greeter struct.
Set Struct Values from Method
Not only can methods read struct values, but struct methods can set struct values:
// dice.go
package main
type Dice struct {
Seed int
}
func (d *Dice) SetSeed(value int) {
d.Seed = value
}
func (d *Dice) GetSeed() int {
return d.Seed
}
// dice_test.go
package main
import "testing"
func TestDice(t *testing.T) {
dice := &Dice{}
dice.SetSeed(1)
if dice.GetSeed() != 1 {
t.Error("Issue with set seed")
}
dice.SetSeed(2)
if dice.GetSeed() == 1 {
t.Error("Issue with set seed")
}
}Method functions can alter values in the struct. In the example above, the dice can have it’s random seed value altered at a regular interval. If there are more than one each can have their own seed or even have matching seeds when a player is on a “hot streak”.
Conclusion
Coming from C, Go feels familiar. C Structs are a specialized array to contain any type of primitive.
Go Structs share a similar property, but because attaching them to methods, they are more powerful. Go Structs can get and set values within the struct, that allows more flexibility while keeping run time code and configuration code separate.
I find this a great way to balance concrete implementation with run-time configuration for a method:
- Run-time configuration can be setup once.
- Method parameters list can be the variable parameters within the run-time, not any setup configuration.
- No need to have ‘globals’ to track initialized method values.
With Go Structs, I start to think of functional components that are self contained.