6

Java to Go in-depth tutorial

 3 years ago
source link: https://yourbasic.org/golang/go-java-tutorial/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java to Go in-depth tutorial

yourbasic.org/golang

This tutorial is in­tended to help Java deve­lopers come up to speed quickly with Go.

Hello stack (example)

Let’s start with a small but complete example. It shows how to implement and use a simple abstract data type in Go.

// Package collection implements a stack of strings.
package collection

// The zero value for Stack is an empty stack ready to use.
type Stack struct {
    data []string
}

// Push adds x to the top of the stack.
func (s *Stack) Push(x string) {
    s.data = append(s.data, x)
}

// Pop removes and returns the top element of the stack.
// It’s a run-time error to call Pop on an empty stack.
func (s *Stack) Pop() string {
    n := len(s.data) - 1
    res := s.data[n]
    s.data[n] = "" // to avoid memory leak
    s.data = s.data[:n]
    return res
}

// Size returns the number of elements in the stack.
func (s *Stack) Size() int {
    return len(s.data)
}
  • Comments that appear directly before top-level declarations are documentation comments. They are written in plain text.
  • For declarations, your write the name followed by the type.
  • A struct corresponds to a class in Java, but the members of a struct cannot be methods, only variables.
  • The code fragment (s *Stack) declares a method receiver s corresponding to Java’s this.
  • The operator := both declares and initializes a variable. Its type is deduced from the initialization expression.

Here is a Hello world program, which shows how to use the collection.Stack abstract data type.

package collection_test

import (
    "fmt"
    "go-for-java-programmers/collection"
)

func ExampleStack() {
    var s collection.Stack
    s.Push("world!")
    s.Push("Hello, ")
    for s.Size() > 0 {
        fmt.Print(s.Pop())
    }
    fmt.Println()
    // Output: Hello, world!
}
  • The test package collection_test resides in the same directory as the collection package.

  • The first import declaration contains the path “fmt” to a standard package; the second one indicates that we will use the package from the directory “go-for-java-programmers/collection”.

  • The packages are accessed within the source file by the short names fmt and collection, respectively.

Note: The idiomatic way to implement a stack in Go is to use a slice directly. See, Implement a stack (LIFO).

Main differences

Object-oriented programming

  • Go does not have classes with constructors. Instead of instance methods, a class inheritance hierarchy, and dynamic method lookup, Go provides structs and interfaces.
  • Go allows methods on any type; no boxing is required. The method receiver, which corresponds to this in Java, can be a direct value or a pointer.
  • Go provides two access levels, analogous to Java’s public and package-private. Top-level declarations are public if their names start with an upper-case letter, otherwise they are package-private.

Functional programming

  • Functions in Go are first class citizens. Function values can be used and passed around just like other values and function literals may refer to variables defined in a enclosing function.

Pointers and references

  • Go offers pointers to values of all types, not just objects and arrays. For any type T, there is a corresponding pointer type *T, denoting pointers to values of type T.
  • Go uses nil for invalid pointers, where Java uses null.
  • Arrays in Go are values. When an array is used as a function parameter, the function receives a copy of the array, not a pointer to it. However, in practice functions often use slices for parameters; slices are references to underlying arrays.
  • Certain types (maps, slices, and channels) are passed by reference, not by value. That is, passing a map to a function does not copy the map; if the function changes the map, the change will be seen by the caller. In Java terms, one can think of this as being a reference to the map.

Built-in types

  • Strings are provided by the language; a string behaves like a slice of bytes, but is immutable.
  • Hash tables are provided by the language. They are called maps.

Error handling

  • Instead of exceptions, Go uses errors to signify events such as end-of-file, and run-time panics for run-time errors such as attempting to index an array out of bounds.

Concurrency

  • Separate threads of execution, goroutines, and communication channels between them, channels, are provided by the language.

Absent features

Syntax

Syntax Terror

Declarations

The declaration syntax is reversed compared to Java. You write the name followed by the type. Type declarations may be read easily from left to right.

Go Approximate Java equivalent var v1 int int v1 = 0; var v2 *int Integer v2 = null; var v3 string String v3 = ""; var v4 [10]int int[] v4 = new int[10];
(Arrays are values in Go.) var v5 []int int[] v5 = null; var v6 *struct{ a int } class C { int a; }
C v6 = null; var v7 map[string]int HashMap<String, Integer> v7;
v7 = null; var v8 func(a int) int interface F {
  int f(int a);
}

F v8 = null;

Declarations generally take the form of a keyword followed by the name of the object being declared. The keyword is one of const, type, var, or func. You can also use a keyword followed by a series of declarations in parentheses.

var (
    n int
    x float64
)

When declaring a function, you must either provide a name for each parameter or not provide a name for any parameter; you can’t omit some names and provide others. You may group several names with the same type.

func f(i, j, k int, s, t string)

A variable may be initialized when it is declared. When this is done, specifying the type of the variable is permitted but not required. When the type is not specified, it defaults to the type of the initialization expression.

var v9 = *v2

If a variable is not initialized explicitly, the type must be specified. In that case it will be implicitly initialized to the type’s zero value (0, nil, ””, etc.). There are no uninitialized variables in Go.

Short declarations

Within a function, a short declaration syntax is available with := . The statement

v10 := v1

is the same as

var v10 = v1

Function types

In Go, functions are first-class citizens. Go’s function type denotes the set of all functions with the same parameter and result types.

type binOp func(int, int) int

var op binOp
add := func(i, j int) int { return i + j }

op = add
n = op(100, 200)  // n = 100 + 200

Multiple assignment

Go permits multiple assignments. The expressions on the right are evaluated before assigning to any of the operands on the left.

i, j = j, i  // Swap i and j.

Functions may have multiple return values, indicated by a list in parentheses. The returned values can be stored by assignment to a list of variables.

func f() (i int, pj *int) { ... }
v1, v2 = f()

The blank identifier

The blank identifier, represented by the underscore character, provides a way to ignore values returned by a multi-valued expression:

v1, _ = f()  // Ignore second value returned by f().

Semicolons and formatting

Instead of worrying about semicolons and formatting, you may use the gofmt program to produce a single standard Go style. While this style may initially seem odd, it is as good as any other style, and familiarity will lead to comfort.

Go code uses very few semicolons in practice. Technically, all Go statements are terminated by a semicolon. However, Go implicitly inserts a semicolon at the end of a non-blank line unless the line is clearly incomplete. A consequence of this is that in some cases Go does not permit a line break. For example, you may not write

func g()
{            // INVALID: "{" should be on previous line.
}

A semicolon will be inserted after g() causing it to be a function declaration rather than a function definition. Similarly, you may not write

if n == 0 {
}
else {       // INVALID: "else {" should be on previous line.
}

A semicolon will be inserted after the } preceding the else, causing a syntax error.

Conditional statements

Go does not use parentheses around the condition of an if statement, or the expressions of a for statement, or the value of a switch statement. On the other hand, it does require curly braces around the body of an if or for statement.

if a < b { f() }
if (a < b) { f() }          // Parentheses are unnecessary.
if (a < b) f()              // INVALID
for i = 0; i < 10; i++ {}
for (i = 0; i < 10; i++) {} // INVALID

Furthermore, if and switch accept an optional initialization statement, which is commonly used to set up a local variable.

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

For statements

Go does not have a while statement nor does it have a do-while statement. The for statement may be used with a single condition, which makes it equivalent to a while statement. Omitting the condition entirely produces an endless loop.

A for statement may contain a range clause for iterating over strings, arrays, slices, maps, or channels. Instead of writing

for i := 0; i < len(a); i++ { ... }

to loop over the elements of a, we could also write

for i, v := range a { ... }

This assigns i to the index and v to the value of the successive elements of an array, slice, or string.

  • For strings, i is an index to a byte and v is a Unicode code point of type rune (rune is an alias for int32).
  • Iterations over maps produce key-value pairs, while channels produce only one iteration value.

Break and continue

Like Java, Go permits break and continue to specify a label, but the label must refer to a for, switch, or select statement.

Switch statements

In a switch statement, case labels do not fall through by default, but you can make them fall through by ending a case with a fallthrough statement.

switch n {
case 0: // empty case body
case 1:
    f() // f is not called when n == 0.
}

But a case can have multiple values.

switch n {
case 0, 1:
    f() // f is called if n == 0 || n == 1.
}

The values in a case can be any type that supports the equality comparison operator, such as strings or pointers. A missing switch expression is equivalent to the expression true.

switch {
case n < 0:
    f1()
case n == 0:
    f2()
default:
    f3()
}

Increment and decrement

The ++ and - - may only be used as postfix operators and only in statements, not in expressions. For example, you cannot write n = i++.

Defer statement

A defer statement invokes a function whose execution is deferred to the moment the surrounding function returns.

  • The deferred function will be executed regardless of which path the surronding function takes to return.
  • The parameters of the deferred function, however, are computed and saved for future use already when the defer statement executes.
f, err := os.Open("filename")
defer f.Close() // f will be closed when this function returns.

Constants

In Go constants may be untyped. This applies to

  • numeric literals,
  • expressions using only untyped constants,
  • and const declarations where no type is given and the initializer expression is untyped.

A value derived from an untyped constant becomes typed when it is used within a context that requires a typed value. This permits constants to be used relatively freely even though Go has no implicit type conversion.

var a uint
f(a + 1)   // The untyped numeric constant 1 becomes typed as uint.
f(a + 1e3) // 1e3 is also typed as uint.

The language does not impose any limits on the size of an untyped numeric constant. A limit is only applied when a constant is used where a type is required.

const huge = 1 << 100
var n int = huge >> 98

If the type is absent in a variable declaration and the corresponding expression evaluates to an untyped numeric constant, the constant is converted to type rune, int, float64, or complex128 respectively, depending on whether the value is a character, integer, floating-point, or complex constant.

c := 'å'    // rune (alias for int32)
n := 1 + 2  // int
x := 2.7    // float64
z := 1 + 2i // complex128

Go does not have enumerated types. Instead, you can use the special name iota in a single const declaration to get a series of increasing value. When an initialization expression is omitted for a const, it reuses the preceding expression.

const (
    red = iota // red == 0
    blue       // blue == 1
    green      // green == 2
)

Structs

A struct corresponds to a class in Java, but the members of a struct cannot be methods, only variables. A pointer to a struct is similar to a reference variable in Java. As opposed to Java classes, structs may also be defined as direct values. In both cases you use . to access the members of a struct.

type MyStruct struct {
    s string
    n int64
}

var x MyStruct     // x is initialized to MyStruct{"", 0}.
var px *MyStruct   // px is initialized to nil.
px = new(MyStruct) // px points to the new struct MyStruct{"", 0}.

x.s = "Foo"
px.s = "Bar"

In Go, methods may be associated with any user-defined type, not just with structs; see the section Methods and interfaces.

Pointers

pointers.jpg

If you have an int, a struct or an array, assign­ment copies the contents of the object. To achieve the effect of Java reference variables, Go uses pointers.

For any type T, there is a corresponding pointer type *T, denoting pointers to values of type T.

To allocate storage for a pointer variable, use the built-in function new, which takes a type and returns a pointer to the allocated storage. The allocated space will be zero-initialized for the type. For example, new(int) allocates storage for a new int, initializes it with the value 0, and returns its address, which has type *int.

The Java code T p = new T(), where T is a class with two instance variables a and b of type int, corresponds to

type T struct { a, b int }
var p *T = new(T)

or the more idiomatic

p := new(T)

The declaration var v T, which declares a variable that holds a value of type T, has no equivalent in Java. Values can also be created and initialized using a composite literal. For example:

v := T{1, 2}

is equivalent to

var v T
v.a = 1
v.b = 2

For an operand x of type T, the address operator &x gives the address of x, a value of type *T. For example:

p := &T{1, 2} // p has type *T

For an operand x of pointer type, the pointer indirection *x denotes the value pointed to by x. Pointer indirections are rarely used; Go, just like Java, can automatically take the address of a variable.

p := new(T)
p.a = 1 // equivalent to (*p).a = 1

Slices

A slice is conceptually a struct with three fields:

  • a pointer into an array,
  • a length,
  • and a capacity.

Slices support the [] operator to access elements of the underlying array.

  • The built-in len function returns the length of the slice.
  • The built-in cap function returns the capacity.

Given an array, or another slice, a new slice is created via a[i:j].

  • This creates a new slice which refers to a, starts at index i, and ends before index j.
  • It has length j - i.
  • If i is omitted, the slice starts at 0.
  • If j is omitted, the slice ends at len(a).

The new slice refers to the same array to which a refers. That is, changes made to the elements using the new slice may be seen using a.

The capacity of the new slice is simply the capacity of a minus i. The capacity of an array is the length of the array.

var s []int
var a [10]int

s = a[:] // short for s = a[0:len(a)]

If you create a value of type [100]byte (an array of 100 bytes, perhaps a buffer) and you want to pass it to a function without copying it, declare the function parameter to have type []byte, and pass a slice of the array. Slices may also be created using the make function as described below.

Slices combined with the built-in function append offer much the same functionality as Java’s ArrayList.

s0 := []int{1, 2}
s1 := append(s0, 3)     // append a single element
s2 := append(s1, 4, 5)  // append multiple elements
s3 := append(s2, s0...) // append a slice

The slice syntax may also be used with a string. It returns a new string whose value is a substring of the original string.

Making values

Map and channel values must be allocated using the built-in make function. For example, calling

make(map[string]int)

returns a newly allocated value of type map[string]int.

As opposed to new, make returns the actual object, not an address. This is consistent with the fact that maps and channels are reference types.

For maps, make takes a capacity hint as an optional second argument.

For channels, there is an optional second argument that sets the buffering capacity of the channel; the default is 0 (unbuffered).

The make function may also be used to allocate a slice. In this case it allocates memory for the underlying array and returns a slice referring to it. There is one required argument, which is the number of elements in the slice. A second optional argument is the capacity of the slice.

m := make([]int, 10, 20) // Same as new([20]int)[:10]

Methods and interfaces

A method looks like an ordinary function definition, except that it has a receiver. The receiver is similar to the this reference in a Java instance method.

type MyType struct { i int }

func (p *MyType) Get() int {
    return p.i
}

var pm = new(MyType)
var n = pm.Get()

This declares a method Get associated with MyType. The receiver is named p in the body of the function.

Methods are declared on defined types. If you convert the value to a different type, the new value will have the methods of the new type, not those of the old type.

You may define methods on a built-in type by declaring a new defined type derived from it. The new type is distinct from the built-in type.

type MyInt int

func (p MyInt) Get() int {
    return int(p) // The conversion is required.
}

func f(i int) {}
var v MyInt

v = v * v // The operators of the underlying type still apply.
f(int(v)) // int(v) has no declared methods.
f(v)      // INVALID

Interfaces

A Go interface is similar to a Java interface, but any type that provides the methods named in a Go interface may be treated as an implementation of that interface. No explicit declaration is required.

Let’s assume that this interface is defined:

type MyInterface interface {
    Get() int
    Set(i int)
}

Since MyType already has a Get method, we can make MyType satisfy the interface by adding

func (p *MyType) Set(i int) {
    p.i = i
}

Now any function which takes MyInterface as a parameter will accept a variable of type *MyType.

func GetAndSet(x MyInterface) {}

func f1() {
    var p MyType
    GetAndSet(&p)
}

In Java terms, defining Set and Get for *MyType made *MyType automatically implement MyInterface. A type may satisfy multiple interfaces. This is a form of duck typing.

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
– James Whitcomb Riley

Embedding (delegation)

Embedding a type as an anonymous field may be used to implement a form of subtyping.

type MySubType struct {
    MyType
    j int
}

func (p *MySubType) Get() int {
    p.j++
    return p.MyType.Get()
}

This effectively implements MySubType as a subtype of MyType.

func f2() {
    var p MySubType
    GetAndSet(&p)
}

The Set method is inherited from MyType, because methods associated with the anonymous field are promoted to become methods of the enclosing type.

In this case, because MySubType has an anonymous field of type MyType, the methods of MyType also become methods of MySubType. The Get method was overridden, and the Set method was inherited.

This is not the same as a subclass in Java, but a form of delegation. When a method of an anonymous field is called, its receiver is the field, not the surrounding struct. In other words, methods on anonymous fields are not dynamically dispatched. When you want the equivalent of Java’s dynamic method lookup, use an interface.

func f3() {
    var v MyInterface

    v = new(MyType)
    v.Get() // Call the Get method for *MyType.

    v = new(MySubType)
    v.Get() // Call the Get method for *MySubType.
}

Type assertions

A variable that has an interface type may be converted to have a different interface type using a type assertion. This is implemented dynamically at run time. Unlike Java, there does not need to be any declared relationship between the two interfaces.

type Printer interface {
    Print()
}

func f4(x MyInterface) {
    x.(Printer).Print() // type assertion to Printer
}

The conversion to Printer is entirely dynamic. It will work as long as the dynamic type of x (the actual type of the value stored in x) defines a Print method.

Errors

Where Java typically uses exceptions, Go has two different mechanisms:

  • most functions return errors;
  • only truly unrecoverable conditions, such as an out-of-range index, produce run-time exceptions.

Go’s multivalued return makes it easy to return a detailed error message alongside the normal return value. By convention, such messages have type error, a simple built-in interface.

type error interface {
    Error() string
}

For example, the os.Open function returns a non-nil error value when it fails to open a file.

func Open(name string) (file *File, err error)

The following code uses os.Open to open a file. If an error occurs it calls log.Fatal to print the error message and stop.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

The error interface requires only an Error method, but specific error implementations often have additional methods, allowing callers to inspect the details of the error.

Panic and recover

A panic is a run-time error that unwinds the stack of the goroutine, running any deferred functions along the way, and then stops the program.

Panics are similar to Java exceptions, but are only intended for run-time errors, such as following a nil pointer or attempting to index an array out of bounds. To signify events such as end-of-file, Go programs use the built-in error type as described above.

The built-in function recover can be used to regain control of a panicking goroutine and resume normal execution:

  • a call to recover stops the unwinding and returns the argument passed to panic.

Because the only code that runs while unwinding is inside deferred functions, recover is only useful inside deferred functions. If the goroutine is not panicking, recover returns nil.

Goroutines and channels

Bouncing balls

Goroutines

Go permits starting a new thread of execution, a goroutine, using the go statement. It runs a function in a different, newly created, goroutine. All goroutines in a single program share the same address space.

Goroutines are lightweight, costing little more than the allocation of stack space. The stacks start small and grow by allocating and freeing heap storage as required. Internally goroutines act like coroutines that are multiplexed among multiple operating system threads.

go list.Sort() // Run list.Sort in parallel; don’t wait for it.

Go has function literals, which can act as closures and are powerful when coupled with the go statement.

// Publish prints text to stdout after the given time has expired.
func Publish(text string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
    }() // Note the parentheses. We must call the function.
}

The variables text and delay are shared between the surrounding function and the function literal; they survive as long as they are accessible.

Channels

A channel provides a mechanism for two goroutines to synchronize execution and communicate by passing a value of a specified element type. The <- operator specifies the channel direction, send or receive. If no direction is given, the channel is bi-directional.

chan Sushi     // can be used to send and receive values of type Sushi
chan<- float64 // can only be used to send float64s
<-chan int     // can only be used to receive ints

Channels are a reference type and are allocated with make.

ic := make(chan int)       // unbuffered channel of ints
wc := make(chan *Work, 10) // buffered channel of pointers to Work

To send a value on a channel, use <- as a binary operator. To receive a value on a channel, use it as a unary operator.

ic <- 3      // Send 3 on the channel.
work := <-wc // Receive a pointer to Work from the channel.
  • If the channel is unbuffered, the sender blocks until the receiver has received the value.
  • If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.
  • Receivers block until there is data to receive.

The close function records that no more values will be sent on a channel:

  • After calling close, and after any previously sent values have been received, receive operations will return a zero value without blocking.
  • A multi-valued receive operation additionally returns an indication of whether the channel is closed.
ch := make(chan string)
go func() {
    ch <- "Hello!"
    close(ch)
}()
fmt.Println(<-ch) // Print "Hello!".
fmt.Println(<-ch) // Print the zero value "" without blocking.
fmt.Println(<-ch) // Once again print "".
v, ok := <-ch     // v is "", ok is false.

In the next example we let the Publish function return a channel, which is used to broadcast a message when the text has been published.

// Publish prints text to stdout after the given time has expired.
// It closes the wait channel when the text has been published.
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
        close(ch)
    }()
    return ch
}

This is how you might use this Publish function.

wait := Publish("important news", 2 * time.Minute)
// Do some more work.
<-wait // blocks until the text has been published

Select statement

The select statement is the final tool in Go’s concurrency toolkit. It chooses which of a set of possible communications will proceed:

  • if any of the communications can proceed, one of them is randomly chosen and the corresponding statements are executed;
  • otherwise, if there is no default case, the statement blocks until one of the communications can complete.

Here is a toy example showing how the select statement can be used to implement a random number generator.

rand := make(chan int)
for { // Send random sequence of bits to rand.
    select {
    case rand <- 0: // note: no statement
    case rand <- 1:
    }
}

Somewhat more realistically, here is how a select statement could be used to set a time limit on a receive operation.

select {
case news := <-AFP:
    fmt.Println(news)
case <-time.After(time.Minute):
    fmt.Println("Time out: no news in one minute.")
}

The function time.After is part of the standard library; it waits for a specified time to elapse and then sends the current time on the returned channel.

Hello server (example)

We end with a small example to show how the pieces fit together. The server package implements a server, which accepts Work requests through a channel:

  • each request is served in a separate goroutine,
  • the Work struct itself contains a channel used to return a result.
package server

import "log"

// New creates a new server that accepts Work requests
// through the req channel.
func New() (req chan<- *Work) {
    wc := make(chan *Work)
    go serve(wc)
    return wc
}

type Work struct {
    Op    func(int, int) int
    A, B  int
    Reply chan int // Server sends result on this channel.
}

func serve(wc <-chan *Work) {
    for w := range wc {
        go safelyDo(w)
    }
}

func safelyDo(w *Work) {
    // Regain control of a panicking goroutine to avoid
    // killing the other executing goroutines.
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(w)
}

func do(w *Work) {
    w.Reply <- w.Op(w.A, w.B)
}

This is how you might use it.

package server_test

import (
    "fmt"
    "server"
    "time"
)

func main() {
    s := server.New()

    divideByZero := &server.Work{
        Op:    func(a, b int) int { return a / b },
        A:     100,
        B:     0,
        Reply: make(chan int),
    }
    s <- divideByZero

    select {
    case res := <-divideByZero.Reply:
        fmt.Println(res)
    case <-time.After(time.Second):
        fmt.Println("No result in one second.")
    }
    // Output: No result in one second.
}

Further reading

learn-to-write-thumb.jpg

Tutorials for beginners and experienced developers alike: best practices and production-quality code examples.

Share:             


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK