Go for Java Programmers: Packages, Functions, and Variables

Go for Java Programmers: Packages, Functions, and Variables

Part of the Go for Java Programmers series

Go’s Familiar Syntax

Due to their shared heritage in the C programming language, Go (aka Golang) code should be fairly recognizable to a Java developer.  Here is the canonical “Hello world” program, which you can execute and modify through your browser on the Go Playground site:

package main

import "fmt"

func main() {
	fmt.Println("Hello world")
}

Functions and control structures begin and end with curly-braces.  Function parameters are enclosed in parentheses, with empty parentheses for functions having zero parameters.  Strictly speaking, Go statements terminate with semicolons just as in Java, although the convention is to let the compiler insert them implicitly.  The naming convention for variables and functions is “camelCase”, rather than using underscores (Python people tend to hate this!).

Other Go fundamentals closely mirror their Java counterparts, too.  Go code is grouped into “packages”.  A function main(), located in the package main, is the entry-point to a Go program… just like the main method in a Java application:

package com.mycompany;

public class MyApplication {

   public static void main(String args[]) {
      System.out.println("Hello world");
   }

}

Packages, Variables, and Functions

However, there are of course differences.  Compare this extended Java code:

package com.mycompany;

import java.util.Date;

public class MyApplication {

	private Date currentTime;

	public static void main(String args[]) {
		MyApplication instance = new MyApplication();
		String message = instance.sayHello("world");
		System.out.println(message);
		System.out.println("The time is currently: " + instance.currentTime.toString());
	}

	public MyApplication() {
		currentTime = new Date();
	}

	private String sayHello(String listener) {
		return "Hello, " + listener;
	}

}

… to a Go counterpart:

package main

import (
	"fmt"
	"time"
)

var currentTime time.Time

func init() {
	currentTime = time.Now()
}

func main() {
	message := sayHello("world")
	fmt.Println(message)
	fmt.Println("The time is currently: " + currentTime.String())
}

func sayHello(listener string) string {
	return "Hello, " + listener
}

As you can see, Go variable and function declarations work in reverse order from Java.  Like all Go functions, the sayHello declaration here begins with the keyword func rather than a return type.  The return type, string in this case, comes at the very end just before the opening curly-brace.  The same is true for function parameters.  Instead of String listener, the parameter declaration is listener string.

Variables are declared with the keyword var, with the type likewise following the variable name. However, line 15 illustrates a convenient shorthand notation.  When the colon-equals operator := is used rather than a plain equals sign, both the var keyword and the variable type can be omitted.  The compiler is smart enough to deduce the correct type from what’s on the right-hand side on the operator.

Scope and Lifecycle

Because the variable message is declared inside of main, its scope is limited to that function.  However, currentTime is declared at the package level, and therefore is visible to all functions in the package.  Notice the lack of access level modifiers on the variables and functions here.  Java uses the keywords public, private, etc to control access from outside of a class or package.  Go does this through capitalization.  A variable or function beginning with a lower-case character is analogous to private in Java.  If the currentTime variable had been declared like this instead:

...
var CurrentTime time.Time
...

… then it would be analogous to Java’s public, and would be accessible from other packages as well.  The same convention applies for functions, as you can see on lines 16 and 17.  Because it starts with an upper-case “P”, the Println function may be used outside of the standard fmt package.

Lastly, notice the init() function on line 10.  Although main() is the primary entry-point for a Go application, init() is a special (optional) function that is automatically called before anything else.  It is typically used to initialize variables, or other setup and validation tasks.  For now, you can think of init() is roughly comparable to a Java constructor, even though in Go the distinction between static and instance methods doesn’t quite apply.  More detail on how Go compares to Java’s object-orientation will come later in this series, in a post about Go types.

Multiple Return Types?!?

Java methods can take any number of input parameters, but always return no more than ONE return type.  If you need a method to return more than one distinct piece of information, then you would either create a custom type to hold the multiple pieces… or else give up and refactor the method to avoid that clutter.

One of the surprises with Go functions is that they quite commonly return multiple values in a single call!  Consider this example function which trims whitespace from a string, and returns both the trimmed string and the number of characters removed in the process:

package main

import (
	"fmt"
	"strings"
)

func main() {
	trimmedString, numberOfCharsRemoved := trimWithCount("This is a test   ")
	fmt.Printf("%d characters were trimmed from [%s]\n", numberOfCharsRemoved, trimmedString)
}

func trimWithCount(input string) (string, int) {
	trimmedString := strings.Trim(input, " ")
	numberOfCharsRemoved := len(input) - len(trimmedString)
	return trimmedString, numberOfCharsRemoved
}

You simply declare a function with a comma-separated list of return types, just as as you would declare a comma-separated list of input parameters.  The return statement must match those types in the correct order.  When the function is invoked, as it is here on line 9, its results should be assigned to a comma-separated list of variables that are likewise in the right order.

As we’ll see later in this series when talking about exception handling, Go uses this approach extensively to report errors.  If something could go wrong within a function, is it typical for the function to return both a result and an error type.  Callers can verify the clean success of the function call by checking that the error is nil (i.e. null):

...
number, err := strconv.Atoi(someNumericString)
if err != nil {
   // Could not parse this string variable as an integer
   log.Fatal(err)
}
...

Deferred Function Calls

Often you have logic that you want to ensure happens at the end of a given block, no matter what.  Java supports this with the try-catch-finally construct.  Code inside of a finally block should always be executed following the code in its try block, even if some exception triggers the catch block in between.

Java 7 enhanced this idea with the “try-with-resources” mechanism.  Resources such an open file handle or a database connection, which are declared at the outset of a try block, are automatically closed even if you don’t do so manually inside of a finally block:

...
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {

	// Do something with the file

} catch(Exception e) {
	log.error("An error occurred while processing the file: " + e);
} finally {
	log.debug("The 'finally' block was reached");
}
...

Go has a similar mechanism, in deferred function calls.  When a function is invoked with the defer keyword, then its execution occurs upon returning from the function in which its invoked.

...
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println("Could not open file")
		return
	}
	defer file.Close()

	// Do something with the file

	return
}
...

Here, the call to file.Close() on line 7 will always be invoked just before the surrounding function returns, even if the surrounding function “panics” (to be covered later in this series in an article on exception handling).  Any function can be deferred in this manner.  Key things to note are that:

  • Multiple deferred function calls are executed in LIFO order.  If another deferred function were scheduled after the file.Close() call on line 10, then that function would be invoked before the file.Close().
  • Any parameters passed to a deferred function are evaluated at the point where the function in scheduled, not the point at which it’s actually executed.

Conclusion

Syntax, packages, functions, and variable in Go are strikingly similar to Java, yet differ in powerful ways.  In the next article of the Go for Java Programmers series, we’ll look at Go’s control structures.