Monday, May 25, 2020

GO Access Control Best Practice




I've been using GO language for some time now, and I've collected some guidelines and code standards to enable code maintainability and flexibility.

As always, guidelines are only guidelines, and can have exceptions on various usages, but still these guidelines usually do save time on later changes to the code, and reduce coupling.


The Guidelines


1. Create new package for each functional struct

What is a "functional struct"?
A functional struct is a structure that has methods attached to it.

For example, in this code section:


package book

import "fmt"

type author struct {
firstName string
lastName string
}

type Book struct {
name string
author author
}

func Create(name string, authorFirstName string, authorLastName string) *Book {
b := Book{
name: name,
author: author{
firstName: authorFirstName,
lastName: authorLastName,
},
}
return &b
}

func (b *Book) Print() {
fmt.Printf("The book name is: %v\n", b.name)
fmt.Printf("Written by: %v %v\n", b.author.lastName, b.author.firstName)
}

  • The Book struct is a functional structure, as it includes a Print method.
  • The author struct is a data storage structure, and it does not include any related methods.

As seen in this example, the Book structure resides in its own package: the book package.
This means that we only expose public elements (functions and structs starting with upper case) to other packages. 

The important idea to understand is that the book package should include ONLY the Book structure related methods and possibly other data storage structures, but nothing else.

This means that we will have many packages in our code, but it significantly reduces the coupling.

The usage of this structure would be as follow:


func main() {
b := book.Create("Alice in Wonderland", "Lewis", "Carroll")
b.Print()
}


2. Use a Create function to construct the structure


As seen in the previous example, the Book structure is created using the Create function.
While this somehow complicates the new structure creation, it allows great flexibility.

For example, let's assume that now we want to create a map of authors by first name. If we would have directly created the structure, we would have to change all of the struct usages to initialize the map, while in our case, we would change it only in a single location:

func Create(name string, authorFirstName string, authorLastName string) *Book {
b := Book{
name: name,
authors: make(map[string]author),
}
b.authors[authorFirstName] = author{
firstName: authorFirstName,
lastName: authorLastName,
}
return &b
}




3. Expose only public entities


This is obvious but should be mentioned.

You package is your fortress. You should open access to the package only where needed.

Hence we will use GO upper case methods, functions, and variables, only where we need.



4. Wire dependencies instead of creating them


Whenever using one function struct on another functional struct, create the structures from out of the function structures scope. 

For example, let's assume we want a printer class for to print the output:


package printer

import (
"fmt"
"io/ioutil"
)

type Printer struct {
outputFile string
printToStdout bool
}

func Create(outputFile string, printToStdout bool) *Printer {
p := Printer{
outputFile: outputFile,
printToStdout: printToStdout,
}
return &p
}

func (p *Printer) Print(format string, a ...interface{}) {
if p.printToStdout {
fmt.Printf(format, a...)
} else {
data := fmt.Sprintf(format, a...)
ioutil.WriteFile(p.outputFile, []byte(data), 0x555)
}
}


So, we create the Printer structure on its own package, as mentioned in guideline #1.
But we do not construct the Printer structure within the Book structure, but only from the outside.
Hence an example of an update usage is:


func main() {
p := printer.Create("", true)
b := book.Create(p, "Alice in Wonderland", "Lewis", "Carroll")
b.Print()
}

and so the updated Print method is using the Printer structure:

func (b *Book) Print() {
b.printer.Print("The book name is: %v\n", b.name)
for _, a := range b.authors {
b.printer.Print("Written by: %v %v\n", a.lastName, a.firstName)
}
}


This allows use to send additional parameters to the Printer structure without modifications of the Book structure.


Final Notes


In this post we have reviewed some coding guidelines for the GO Access Control.
These guidelines might appear some cumbersome at first sight, but in the long distance save a lot of time in bug fixes, and maintainability. 

If you like the ideas presented here, leave a comment!









4 comments:

  1. About b := book.Create(p, "Alice in Wonderland", "Lewis", "Carroll")
    The book Create function gets a reference to the printer and a new method was added to book - the Print method. book.Print() must be using the printer that was sent as the first argument of Create(..). It may help if you would share the book.Print() implementation

    ReplyDelete
  2. The `Create` function in the book package has a signature that accepts three strings, but in the main function, it's being called with variable `p` of type `Pointer` and then three strings. How does that work?

    Also the `Book` struct does not have a field called `printer` but it's being referenced in the `Print` method's implementation. How did it show up?

    I think you meant to show that you can change things about `Book` in one place while making use of Dependency Injection to decouple from something like a `Printer`. But, you left out those changes to the reader. The code defined in the book package does not match how you are using it the main function or that bit about the new `Print` method on `Book`.

    ReplyDelete
    Replies
    1. Thanks for your comment, I've updated the missing usage to clarify it.

      Delete