Chapter01 - pslpune/golang-jumpstart GitHub Wiki

Program structure

Names

Names of functions, variables, constant, types, statement labels, packages follow a simple rule. - name begins with a UTF-8 letter or an underscore and may have number of additional letters, digits and underscores. Also since Golang is case sensitive - heapSort and HeapSort are entirely different names

Go has about 25 keywords and those cannot be used as names when you program.

break       default     func        interface       select
case        defer       go          map             struct
chan        else        goto        package         switch
const       fallthrough if          range           type
continue    for         import      return          var

In addition to the keywords, Go also has predeclared names - these are not reserved and you can have these names but its strictly bound to context. Beware as this might also lead to a lot of confusion.

Constants:      true, false, iota, nil
Types    :      int, int8, int32, int64
                uint8, uint16, uint32, uint64,
                float32, float64, complex128, complex64
                bool, byte, rune, string, error
Functions:      make, len, cap, new, append, copy, close, delete,
                complex, real, imag, panic, recover

If a name appears inside the function/package its local(lifecycle) to that function, but can also be accessed from the outside. The way to protect it is by case of the first letter in the name. Example: const declared as delay inside a package is private to the package and no other external package can see it. while the same constant declared as Delay would mean external packages can access it.

Conventionally - Go prgrammers use short, very short names : though there is no limit as to how long the name could be. Its quite normal to have i as the index of the loop and not index

Also Go programmers prefer camel case when forming names with 2 or more words. Also acronyms are all caps!

Preferred

  • ✔️ QuoteRuneToASCII
  • ✔️ quoteRuneToASCII

Not preferredooooo

  • ❌ quote_rune_toASCII
  • ❌ QuoteRuneToAscii

Declarations

A declaration names an entity, and specifies some/all of its properties. There are 4 major kinds of declarations

  1. var
  2. const
  3. type
  4. func

Go program is stored in one / more .go files. Each file begins with a package declaration that specifies which package is the file part of.

Variable declarations

A var declaration creates a variable of a particular type, attaches a name to it and sets its initial value.

var name type = expression

Either the type or the expression part of it can be omitted but not both. If the type is omitted the initializer expression can help identify the type while if the initializer expression is omitted then the variable would be assigned the respective zero value

  • incase of numerals = 0
  • incase of strings = ""
  • incase of interface and reference types = nil (slice, channel, map, function)
  • incase of aggregates - struct,array it has all zero value of the elements

The "zero" vaue mechanism is very helpful in that it assigns a well defined value to any variable the moment it is initialized, and that there isnt anything called as undefined variable in Go

Its possible to declare multiple variables in a single statment when there are more than one variable that needs intialization.

var name, email, location string // name = "" ,email = "" , location =""
var delay, ok, name =  3.5, true, "niranjan"

Short variable declarations

Only within the functions there is what is called as the short hand of the above variable declaration. Type of the variable here is omitted and inferred from the assignment expression

freq := rand.Float() * 3.0
t := 0.0

As you can notice this a popular choice of declaring local variables with most of the Go programmers. Short declaration also applies to multi-declaratio

i,j := 0,3

Short hand declarations can also be used when calling functions

f, err := os.Open("path/to/file", 667)

A subtle but important point, that short hand declaration does not necessarily always declare new variables on the left side. If the variable with the same name is already declared within the same lexical scope, then it only assigns.

f, err := os.Open("path/to/file", 667)
f2, err := os.Create("path/to/someother/file")

As you can see, err here was declared on the previous line, but in the following one it actually only assigns the value to the err variable. There is a caveat though in this. Following code would not compile.

f, err := os.Open("path/to/file", 667)
f, err := os.Create("path/to/someother/file")

Atleast one variable on the left should be a new declaration - compiler will flag this an error. Correct way is as below.

f, err := os.Open("path/to/file", 667)
f, err = os.Create("path/to/someother/file") // pure assignment statement

Func declarations

Any function / procedure has 3 components, - signature, body, call. Here since we are discussing declarations we focus on signature. A signature has

  • Name of the function
  • Parameters it takes in
  • return values tha it spits out
func DoThis(param1 string, param2 int, flag bool) (int, error){

}

As you can notice, in Go there can be multiple return values. The func keyword is important. Also Go has made functions as first class citizens and that they can passed into other function as params.

func SummTop(a,b) int {
    // summation of number from b-a but the first 2 values
}
func SummBottom(a,b) int {
    // Summation of number from b-a but last 2 values
}
func Amplify(a,b int Summation func(a,b int)int) int {
   return 4*Summation(a,b) 
}

func main(){
    Amplify(3,10, SummBottom) // almost a polymorphic behaviour 
}

As you can notice this lets you keep the context where it is and move around an entire function along just like it was polymorphic. Whats interesting in Go is that you can have functions with variable number of arguments calling it. When specifying about the params we indicate variable params as so

func Concatenate(texts ...string) string {
    for _, s := range texts ...{

    }
}

Return types of the functions can be named in Go. This saves us declaring the variables for return inside the function.

func SomeCalculation(a, b float) (result int, e error){
    result  = a+b 
    e =nil
    return result, e
}

Type declaration

Type of the variable signifies

  • Data type and the characteristics of the data type (size in the memory) it would be taking on
  • How they are represented internall in the memory
  • Intrinsic operations that can be performed on them

In any program there are variables that have the same representation but signify different concecpts. For instance an int represents an Integer but it can be a loop index, timestamp, file descriptor, or month. A string could be a name, email, color, or location.

A type declaration defines a new named type that has the same underlying type as an existing type. Such that they cannot be unintentionally mixed and used for the same purpose.

import "fmt"

// 2 types of floats, same underlying types but different domain representations 
// this is it  avoid mixing and interchangeably using the value without conversion 
type Celcius float64 
type Fahrenheit float64 

const (
    AbsoluteZeroC Celcius  = -273.15
    FreezingC Celcius = 0.0
    BoilingC Celcius = 100.0
)
func CToF(c Celcius) Fahrenheit {
    return Fahrenheit(c *9/5+32)
}
func FtoC(f Fahrenheit) Celcius {
    return Celcius((f-32)*5/9)
}

Celcius and Fahrenheit have the same underlying type but they cannot be compared in an arithmetic expression. Distinguishing the types on their domain representaion makes it possible to avoid inadvertently combining 2 values of temperatures on different scales. If you had to force convert the values - disregarding the scales of temperature you would have to make an explicit conversion

t := Celcius(0.0)
f = Fahrenheit(t) // you made your intentions clear here, you want a forced conversion disregarding the temp scales.

The value above would be now 0 deg Fahrenheit, perhaps not was intended in real world, but since you have made an explicit conversion its assumed thats what you intend.

Package & Files

Packages serve as the same purpose as the libraries / modules in other languages. - supporting

  1. Modularity
  2. Re-Use
  3. Encapsulation
  4. Separate compilation

Source code in a package resides in one / more .go files usually in a directory whose name resembles the import path. Creating a new package under a module is as simple as creating a directory under a module and has atleast one go file with package <name>

In directory tempconv we make this go file & begin by specifying package name tempconv. For all future references our package can be referred by anyone on the internet as "module/path/package_name". So for example If I have a module registered as github.com/kneerunjun/services and the below package can be publicly referred to as github.com/kneerunjun/services/tempconv

package tempconv

import "fmt"

type Celcius float64 
type Fahrenheit float64 

const (
    AbsoluteZeroC Celcius  = -273.15
    FreezingC Celcius = 0.0
    BoilingC Celcius = 100.0
)
func CToF(c Celcius) Fahrenheit {
    return Fahrenheit(c *9/5+32)
}
func FtoC(f Fahrenheit) Celcius {
    return Celcius((f-32)*5/9)
}

Package initilization begins by initializing the package level variables in order which they are declared, except that the dependencies are resolved first. If the package has multiple go files they are compiled in a sequence sorted by the go tool.

Every package then also has the init function which isnt actually callable but is called internall whenever the package is being initialized. Typical tasks that can be done inside the init function are

  1. log setup , config
  2. reading environment variables
  3. testing database singular connections

Scope

Declaration associates a name with a programming entity, plus where its declared also has a lot of significance. A syntatic block of code is that refers to a code that is surrounded by the curly braces. - function or loop. A name declared inside such a block is not visible outside the block. The block encloses the declarations, and determines their scope.

Braced blocks together with other blocks that are also encapsulating a scope but havent got any braces surrounding are referred to as what is called lexical scope

A program often contains multiple declarations of the same name (but not inside the same lexcical scope). Here to determine the preceedence the compiler will prefer the innermost scope (cascaded scope) and the declaration that is innermost will take preceedence when execution in that scope.

func main(){
    x:= "hello!"
    for i :=0; i< len(x);i++{
        x := x[i]
        if x != '!' {
            x := x + 'A'- 'a'
            fmt.Printf("%c", x) // "HELLO" one character at a time 
        }
    }
}

Lets pick the distinct lexical scopes in the above snippet

This one is the top local scope under the main function. The x declared here belongs to this scope

func main(){
    x := "hello"
}

The for loop creates another one, x declared here (left side of the := operator) belongs to this scope,while the x appearing in len(x) and x[i] are from the previous scope. - we shall come to this a bit later but for now keep this in mind.

for i :=0; i< len(x);i++{
    x := x[i]
}

The if condition then creates the last cascaded one. here x + 'A'- 'a' x in the expression is from the previous scope as well as the x in x != '!'

    if x != '!' {
        x := x + 'A'- 'a'
        fmt.Printf("%c", x) // "HELLO" one character at a time 
    }

For all practical and general purposes it is very unlikely you would name your variables identical thru cascaded scopes, but this example is to show you how scopes influence the value of the variables and which ones are the referring to.

var cwd string

func init (){
    cwd, err := os.Getwd()
    if err !=nil{
        log.Fatalf("error getting the working directory %s", err)
    }
    log.Printf("current working directory %s", cwd)
}

Compiler will complain (cwd is declared but not used), with a red squiggly line under cwd on the outer scope. Isn't the cwd inside init being used ? := operator would redeclare a new variable local to its scope again with the same name. If you really want to assign cwd, and not declare a new variable you would have to get away from the := operator

var cwd string

func init (){
    var err error 
    cwd, err = os.Getwd()
    if err !=nil{
        log.Fatalf("error getting the working directory %s", err)
    }
    log.Printf("current working directory %s", cwd)
}

With the = operator err has no valid declaration, and hence to ger around the problem on a separate line we have independently declared err as error

Exercise-01

Along with the lecturer we are going to setup the demo project step by step. At the end of it we would have a simple Http JSON API endpoint running on GO This would require you have a environment setup locally or on one of the playgrounds. Alternatively we also encourage you to start using github codedspaces.

⚠️ **GitHub.com Fallback** ⚠️