DEV Community

Dawn Zhao for MarsCode

Posted on • Edited on

Personal experience sharing on Transitioning from Java to Go

Writtern by HaojunFang

Background

I still recall when I first started learning programming, a common phrase I used to hear was: "Programming languages are just tools, the programming mindset is what matters most". However, from a practical perspective, the barriers between different languages are pretty significant. (Otherwise, wouldn't the design of these languages be in vain? 😄).
As a former Java developer, I have a high regard for Java. This probably stems from its robust ecosystem and impressive productivity. Java's syntax is clear and concise, easy to learn, and offers a great deal of flexibility and extensibility simultaneously. In addition, Java's virtual machine mechanism allows it to be cross-platform, capable of running on different operating systems, which is a tremendous advantage for developers.
However, considering the current situation, reality compelled me to learn a new language - Golang. Many companies are favoring Go, and its developers tend to have higher salaries. After almost a year of studying, using, and practicing Go, I have garnered some basic insights. I present them in this article with the hope of sharing and discussing them with you all!

Why Go

Image description

We can observe that Go is relatively low on the list, yet many companies still choose Go as their primary development language. This piqued my interest and I aim to understand the reasons behind this.

After referring to multiple documents, I concluded the following key advantages of the Go language for developers:

  1. Powerful Concurrency: Go's Goroutines and Channels adeptly address the issues related to concurrency and asynchronous programming.
  2. Straightforward Syntax: Go language's syntax is simpler and easier to learn than Java, which can help reduce development costs.
  3. Efficient Memory Management: The memory management in Go is more efficient than Java, thus conserving system resources.
  4. Cross Platform: Go allows for cross-platform compilation, enabling the operation on various operating systems. This makes business expansion and migration more convenient.

Language Features

When it comes to language features, as a former Java developer, a comparison with Java is inevitable:

  1. Go, also known as Golang, is a static, compiled, and concurrent programming language developed by Google. It was designed with simplicity and efficiency in mind. Its features include easy learning curve, efficient concurrency, garbage collection mechanism, and a robust standard library.
  2. Java, on the other hand, is a widely used object-oriented programming language aiming to "write once, run anywhere". It boasts cross-platform capability, object-oriented characteristics, and a rich, powerful library.

OOP

Firstly, Go is not an object-oriented language, but it can simulate object-oriented characteristics using structures and interfaces. In Go, structures hold the same level of importance as classes in object-oriented paradigms. They can contain variables and methods of various types. Object-oriented principles can be achieved through encapsulation, interfaces, and other means.

Object-oriented programming is a paradigm that encapsulates entities with similar features into a unified class form. The properties and methods within the encapsulated classes represent the common characteristics and behaviors among these entities. It primarily encompasses three characteristics: Encapsulation, Inheritance, and Polymorphism.

1. Encapsulation

In OOP, the members and methods of a class can be encapsulated, and their visibility can be controlled by access modifiers. In Go, members of a struct are exposed by default, and can be accessed outside of the struct. Strict encapsulation, like in classes, is not available. However, control of whether to export can be handled via the case of the first letter of the attribute (if not exported, it cannot be accessed externally).

package test111

type TestDto struct {
    age  string // Accessible within the package
    Name string // Accessible globally
}
Enter fullscreen mode Exit fullscreen mode

2. Inheritance

In object-oriented programming languages, object inheritance is used, and in Java, the keyword "extend" is used to achieve this. In Go, inheritance relationships can be implemented using embedded structures.

package test111

type BaseRespDto struct{
    ReturnCode string
    Returnsg  string
}

type QueryOrderRespDto struct{
    BaseRespDto // After composing an anonymous base structure, the current structure can access the attribute fields of the base structure.

    OrderID     string
    OrderAmount int64
}
Enter fullscreen mode Exit fullscreen mode

3. Polymorphism

OOP supports polymorphism, wherein an object can exhibit different behaviors depending on the context. While Go's structs do not directly support polymorphism, a similar effect can be achieved using interfaces. If a type implements all the methods of an interface, it can be assigned to a variable of that interface type. Polymorphism enables programs to automatically select the appropriate method implementation based on the context, thereby enhancing the flexibility and reusability of the code.

var (
    applePhone *Mobile
)

func init() {
    applePhone = NewMobile("xiaoming", "iPhone 15 Pro MAX", 1111100)
}

type USB interface {
    Storage()
    Charge() string
}

type PlayBoy interface {
    Game(name string)
}

type Mobile struct {
    User  string `json:"user"`
    Brand string `json:"brand"`
    Prise int64  `json:"prise"`
}

func NewMobile(user string, brand string, prise float64) *Mobile {
    return &Mobile{User: user, Brand: brand, Prise: prise}
}

func (m *Mobile) CallUp() {
    fmt.Printf("%s is using 💲%.2f mobile phone to make a call.\n", m.User, m.Prise)
}

func (m *Mobile) Storage() {
    fmt.Printf("%s is using a %s mobile phone to transfer data.\n", m.User, m.Brand)
}

func (m *Mobile) Charge() string {
    return fmt.Sprintf("%s is charging a %s phone.\n", m.User, m.Brand)
}

func (m *Mobile) PlayGame(name string) {
    fmt.Printf("%s is playing the game of '%s'.\n", m.User, name)
}

func TestExample(t *testing.T) {
    applePhone.Storage()
    applePhone.Charge()
    applePhone.PlayGame(applePhone.User)
}
Enter fullscreen mode Exit fullscreen mode

Design Philosophy

Design Characteristics of Go
If you can't find an X feature from other languages in Go, it likely means that this feature is not suitable for Go. For instance, it might affect the compilation speed, clarity of design, or it could make the basic system particularly complex.

  1. Less is More: If a feature does not provide significant value in solving any problem, Go will not offer it. If a feature is needed, there should be only one way to implement it.
  2. Interface-Oriented Programming: Go proposes non-intrusive interfaces, rebutting inheritance, virtual functions and their overloading (polymorphism), and eliminating constructors and destructors.
  3. Orthogonal + Composition: Features of the language are independent of each other, not influencing one another. For instance, types and methods are independent, so are the different types with no subclasses, and packages do not have sub-packages. Different features are loosely coupled through composition.
  4. Concurrency Support at the Language Level: Concurrency better utilizes multi-cores and offers a robust expressiveness to simulate the real world.

Common Pitfalls & Knowledge Points at Syntax Level

Types

In addition to the common basic types, Go also has a Func() type that represents a function. This is somewhat similar to the Supplier functional interface in Java.

Branching Decision

  • switch-case-default

The switch-case syntax in Go is fundamentally similar to that in Java, with minor distinctions between the two. Let's clarify this with an example:

Go

func main() {
    name := "ddd"
    switch name {
    case "xxxx":
       fmt.Println(name + "1")
    case "ddd":
       fmt.Println(name + "2")
    case "xxxxxx":
       fmt.Println(name + "3")
    default:
       fmt.Println(name + "4")
    }
}

Output Result:
ddd2
Enter fullscreen mode Exit fullscreen mode

Java

public static void main(String[] args) {
    testSwitch("name");
}
private static void testSwitch(String var){
    switch (var){
        case "1":
            System.out.println(var+"1");
        case "name":
            System.out.println(var+"2");
        case "3":
            System.out.println(var+"3");
        default:
            System.out.println(var+"4");
    }
}

Output Result
name2
name3
name4
Enter fullscreen mode Exit fullscreen mode

In Go, case branches do not explicitly need a break or return statement. Once a rule is met, subsequent branches won't be executed. This is due to Go's design philosophy that emphasizes simplicity and efficiency, avoiding unnecessary code redundancy.
When a switch-case statement in Go encounters a matching case, it automatically executes all the statements in that case until it bumps into the next case or default. If there is no following case or default, the program will continue to execute the following code.
This design approach simplifies the code while preventing errors that can arise from forgetting to add a break statement within a case. However, it is crucial to note that if you need to transition to the next case within a case, you can use the fallthrough keyword to achieve this.

Exported Names
Go does not have access modifiers like Java's public/private/protected/default. Instead, it uses a simple rule where the capitalization of the first letter determines if a structure/variable/method/function is exported. An exported entity, in this context, refers to whether it can be called/used by external packages.
In Go, identifiers of variables, functions, and structures that start with an uppercase letter can be accessed by code outside their package. In contrast, identifiers starting with lowercase are package-private. This is one of the design principles in Go programming language.

Error and Panic

  1. The cumbersome error handling logic in Golang is a point of contention for many Java developers. Every error needs to be manually dealt with in the code, often using the classic if err != nil check.
  2. Golang's panic mechanism, which leads to process exit, also tends to alarm and frustrate most developers.

In my understanding, Error in Go is somewhat analogous to RuntimeException in Java. It represents business errors that occur during runtime, which developers can manually handle. On the other hand, Panic in Go is similar to Error in Java, representing system-level errors that cannot be resolved.

Struct Creation
👍 In Go, when creating variables of struct types, built-in syntax allows for setting field values, so there's no need to manually write constructors or call a host of set methods for assignment like in Java.

Generally, there are two methods for creating an empty instance:

  • new(T)
  • &T{} —— This method is most commonly used.

Value and Reference Pass

In Golang, all parameters are passed by value, i.e., a clone of the original value. Modifications inside functions/methods do not affect the external value objects. Therefore, it's generally recommended to use pointers as arguments in functions/methods due to a couple of benefits:

  1. Reduces memory usage, since passing by value requires duplication.
  2. Allows the modification of specific data pointed to by the pointer inside the function.

Defer Function
The purpose of defer is to postpone the execution of a segment of code, and it's generally used for closing resources or executing necessary finalizing operations. The defer code block is executed no matter whether there is an error or not, which is similar to the function of the finally block in Java.
Defer uses a stack to manage the code that needs to be executed, hence the order of execution of deferred functions is opposite to the order of their declaration - following a "last in, first out" approach.

Pitfalls:
The value of the parameters in a defer function is determined at the time of declaring defer. When declaring a defer function, there are two ways to reference external variables: as function parameters and as closure references.

  • As function parameters, the value is passed to defer at the time of declaration, and the value is cached. The cached value is used for computation when the defer is called.
  • As closure references, the current value is determined based on the entire context when the defer function is executed (this method is recommended).

Comparison with Java

Code Style & Coding Conventions
Compared with Java, Golang has more stringent coding constraints. If such rules are not followed, your code might not be able to compile. The specific constraints are as follows:

  • A defined variable must be used or it can be replaced with _ to denote it can be ignored.
  • Imported packages must be used.
  • Braces must start at the end of the current line, and cannot be placed on a new line.

Access Control
Compared to Java, Golang's access control is straightforward - it only requires distinguishing between uppercase and lowercase initials.

Instance/Object Declaration and Creation
In Java, almost all object instances are created using the new keyword. On the other hand, in Golang, it's simpler - you just need to use a := T{} or a := &T{}

Difference between Structs and Classes
In Java, classes are foundational, and all logic relies on classes for existence.
However, in Golang, there is no concept of a class. The closest definition is a struct. Still, Golang's struct basically serves as a data carrier. In Golang, functions are the basis and can exist independent of structs. Also, in Java, invocations are driven based on classes or objects, whereas in Golang, function/method calls are partitioned by packages.

Functions and Methods
In Java, the term 'function' might be rarely used because all methods rely on the existence of class objects (this gap was filled after Java 8 with functional interfaces). In Golang, however, functions are first-class citizens, and methods are just special functions which include the current receiver as an implicit first parameter.

Pointers
For Java developers, pointers may appear slightly unfamiliar. You can consider it equivalent to object references in Java. Both Java and Golang pass values when making method calls. In Java, if a reference type (object, array, etc.) is passed, its pointer will be copied and passed. In Golang, you must explicitly pass the pointer of the struct; otherwise, you are just passing a copy of the object.

AOP/IOC/Transactional Declaration
One of the aspects making Golang challenging for Java developers is the lack of AOP (Aspect-Oriented Programming) capabilities. For example, to start a DB transaction in Golang, you need to manually write tx begin, tx end, tx rollback, and obtain the tx instance pointer. You also need to ensure that tx is not written incorrectly within a transaction, otherwise deadlocks and inconsistent queries might occur.
In contrast, in Java - thanks to AOP capabilities and the Spring framework - developers generally just need to focus on specific business logic and decide where to add a @Transactional annotation on a particular repo method. The subsequent transaction operations are managed by the framework.

I remember when I first started writing requirements, I encountered an embarrassing situation due to my lack of understanding. I couldn't query the data right after inserting it in the same transaction, which stalled my unit test for quite some time. Eventually, I found out that the SQL query didn't use the same tx connection.

IOC/DI (Inversion of Control/Dependency Injection) essentially implies the handing over of object creation and dependency injection to Spring during application startup in Java. Although Golang has the Wire injection framework, developers essentially need to predefine the instantiation rules, which could be viewed as a compile-time capability.

Sponsored by MarsCode
Welcome to join our Discord to discuss your ideas with us.

Top comments (0)