# Go: Basics for Java/Python Developers in 2026 > Learn Go quickly by leveraging your Java or Python experience. Goroutines, channels, interfaces and essential patterns explained for a smooth transition. - Published: 2026-01-19 - Updated: 2026-03-31 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, backend - Reading time: 12 min --- Go (or Golang) has established itself as the language of choice for microservices, CLI tools, and distributed systems. Created by Google in 2009, it combines Python's simplicity with C-level performance. For developers coming from Java or Python, transitioning to Go is surprisingly smooth once the core concepts click into place. > **Why Go in 2026?** > > Go powers Docker, Kubernetes, Terraform, and countless cloud infrastructures. Its fast compilation, native concurrency support, and single-binary deployment make it ideal for modern backend development. ## Installing and Setting Up Go Installing Go is straightforward and consistent across all platforms. The `go` tool handles compilation, dependency management, and testing. ```bash # install.sh # Installation on macOS with Homebrew brew install go # Installation on Linux (Ubuntu/Debian) sudo apt update && sudo apt install golang-go # Verify installation go version # go version go1.22.0 linux/amd64 ``` Go project structure follows strict but simple conventions. The `go.mod` file defines the module and its dependencies. ```bash # project-setup.sh # Create a new project mkdir my-project && cd my-project go mod init github.com/user/my-project # Generated structure: # my-project/ # ├── go.mod # Module manifest # └── main.go # Entry point # Essential commands go build # Compile the project go run main.go # Compile and execute go test ./... # Run all tests go fmt ./... # Format code automatically ``` ## First Go Program Here is a simple program illustrating Go's basic syntax. Comparing with Java and Python helps visualize the differences. ```go // main.go package main import "fmt" // Program entry point func main() { // Declaration with type inference message := "Hello, Go!" fmt.Println(message) // Explicit declaration var count int = 42 fmt.Printf("Count: %d\n", count) } ``` What stands out immediately: no semicolons, no parentheses around conditions, and type inference with `:=`. Go prioritizes conciseness without sacrificing readability. ## Variables and Fundamental Types Go is statically typed but offers excellent type inference. Basic types cover most use cases. ```go // types.go package main import "fmt" func main() { // Short declaration (inside functions only) name := "Alice" // string age := 30 // int height := 1.75 // float64 active := true // bool // Explicit declaration var score int = 100 var rate float64 = 3.14 // Multiple declaration var ( firstName string = "Bob" lastName string = "Smith" points int = 0 ) // Zero values (default values) var count int // 0 var text string // "" (empty string) var flag bool // false var ptr *int // nil fmt.Println(name, age, height, active) } ``` > **Zero Values in Go** > > Unlike Java or Python, Go automatically initializes variables to their "zero value": 0 for numbers, "" for strings, false for bools, nil for pointers and slices. ## Functions and Multiple Returns Go allows returning multiple values, a feature used extensively for error handling. ```go // functions.go package main import ( "errors" "fmt" ) // Simple function with typed parameters func add(a, b int) int { return a + b } // Multiple returns (idiomatic pattern for errors) func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } // Named returns func getUser(id int) (name string, age int, err error) { if id <= 0 { err = errors.New("invalid ID") return } name = "Alice" age = 30 return } // Variadic function func sum(numbers ...int) int { total := 0 for _, n := range numbers { total += n } return total } func main() { // Simple call result := add(5, 3) fmt.Println("5 + 3 =", result) // Explicit error handling quotient, err := divide(10, 3) if err != nil { fmt.Println("Error:", err) return } fmt.Printf("10 / 3 = %.2f\n", quotient) // Ignore a returned value with _ name, _, _ := getUser(1) fmt.Println("User:", name) // Variadic call total := sum(1, 2, 3, 4, 5) fmt.Println("Sum:", total) } ``` ## Structs and Methods Structs are the building blocks for custom types in Go. Methods attach to types via receivers. ```go // structs.go package main import "fmt" // Struct definition type User struct { ID int Username string Email string Active bool } // Constructor (convention: NewTypeName) func NewUser(id int, username, email string) *User { return &User{ ID: id, Username: username, Email: email, Active: true, } } // Method with value receiver (copy) func (u User) FullInfo() string { status := "inactive" if u.Active { status = "active" } return fmt.Sprintf("%s <%s> (%s)", u.Username, u.Email, status) } // Method with pointer receiver (modification possible) func (u *User) Deactivate() { u.Active = false } // Method with pointer receiver for modification func (u *User) UpdateEmail(newEmail string) { u.Email = newEmail } func main() { // Create with constructor user := NewUser(1, "alice", "alice@example.com") fmt.Println(user.FullInfo()) // Modify via method user.Deactivate() fmt.Println(user.FullInfo()) // Direct creation user2 := User{ ID: 2, Username: "bob", Email: "bob@example.com", } fmt.Println(user2.FullInfo()) } ``` > **Value vs Pointer Receiver** > > Use a pointer receiver (`*User`) when the method modifies state or when the struct is large. Use a value receiver (`User`) for read-only methods on lightweight structs. ## Interfaces: Implicit Polymorphism Go interfaces are implemented implicitly. A type satisfies an interface if it implements all its methods, without explicit declaration. ```go // interfaces.go package main import ( "fmt" "math" ) // Interface definition type Shape interface { Area() float64 Perimeter() float64 } // Rectangle implements Shape implicitly type Rectangle struct { Width, Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) } // Circle also implements Shape type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius } // Function accepting the interface func PrintShapeInfo(s Shape) { fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter()) } func main() { rect := Rectangle{Width: 10, Height: 5} circle := Circle{Radius: 7} // Polymorphism via interface PrintShapeInfo(rect) PrintShapeInfo(circle) // Slice of interfaces shapes := []Shape{rect, circle} for _, shape := range shapes { PrintShapeInfo(shape) } } ``` This approach differs radically from Java where `implements` is mandatory. In Go, conformance is structural, not nominal. ## Slices and Maps: Dynamic Collections Slices are dynamic views over arrays, and maps are native hash tables. ```go // collections.go package main import "fmt" func main() { // Slice: dynamic array numbers := []int{1, 2, 3, 4, 5} // Append elements numbers = append(numbers, 6, 7) // Slicing (similar to Python) subset := numbers[1:4] // [2, 3, 4] fmt.Println("Subset:", subset) // Create slice with make scores := make([]int, 0, 10) // len=0, cap=10 scores = append(scores, 100, 95, 88) // Iteration with range for index, value := range numbers { fmt.Printf("numbers[%d] = %d\n", index, value) } // Map: hash table users := map[string]int{ "alice": 30, "bob": 25, } // Add/Update users["charlie"] = 35 // Check existence age, exists := users["alice"] if exists { fmt.Println("Alice's age:", age) } // Delete delete(users, "bob") // Map iteration for name, age := range users { fmt.Printf("%s is %d years old\n", name, age) } } ``` ## Idiomatic Error Handling Go has no exceptions. Errors are values returned explicitly, forcing rigorous handling. ```go // errors.go package main import ( "errors" "fmt" "os" ) // Sentinel error (for comparison) var ErrNotFound = errors.New("resource not found") var ErrInvalidInput = errors.New("invalid input") // Custom error with context type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) } // Function returning different error types func GetUser(id int) (string, error) { if id <= 0 { return "", &ValidationError{ Field: "id", Message: "must be positive", } } if id > 1000 { return "", ErrNotFound } return "Alice", nil } // Error wrapping (Go 1.13+) func ReadConfig(path string) ([]byte, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config %s: %w", path, err) } return data, nil } func main() { // Basic pattern user, err := GetUser(-1) if err != nil { fmt.Println("Error:", err) // Type assertion for custom error var valErr *ValidationError if errors.As(err, &valErr) { fmt.Printf("Field: %s\n", valErr.Field) } // Comparison with sentinel error if errors.Is(err, ErrNotFound) { fmt.Println("User not found") } } else { fmt.Println("User:", user) } } ``` > **errors.Is and errors.As** > > Since Go 1.13, use `errors.Is()` to compare with sentinel errors and `errors.As()` to extract a specific error type from a wrapped error chain. ## Goroutines: Lightweight Concurrency Goroutines are lightweight threads managed by the Go runtime. Launching a goroutine costs only a few KB of memory. ```go // goroutines.go package main import ( "fmt" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrement counter when done fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup // Launch 5 goroutines for i := 1; i <= 5; i++ { wg.Add(1) go worker(i, &wg) // 'go' prefix launches the goroutine } // Wait for all goroutines to complete wg.Wait() fmt.Println("All workers completed") } ``` The `sync.WaitGroup` allows waiting for multiple goroutines to finish. This is the basic pattern for parallelism in Go. ## Channels: Goroutine Communication Channels are typed conduits for communication between goroutines. They enable safe synchronization and data exchange. ```go // channels.go package main import ( "fmt" "time" ) func producer(ch chan<- int) { for i := 1; i <= 5; i++ { fmt.Println("Producing:", i) ch <- i // Send on channel time.Sleep(100 * time.Millisecond) } close(ch) // Close channel when done } func consumer(ch <-chan int, done chan<- bool) { for value := range ch { // Iterate until closed fmt.Println("Consuming:", value) } done <- true } func main() { ch := make(chan int) // Unbuffered channel done := make(chan bool) go producer(ch) go consumer(ch, done) <-done // Wait for consumer to finish fmt.Println("All done") } ``` ### Buffered Channels and Select ```go // channels_advanced.go package main import ( "fmt" "time" ) func main() { // Buffered channel (capacity 3) buffered := make(chan int, 3) buffered <- 1 buffered <- 2 buffered <- 3 // buffered <- 4 // Would block since buffer is full fmt.Println(<-buffered) // 1 // Select: channel multiplexing ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(100 * time.Millisecond) ch1 <- "from ch1" }() go func() { time.Sleep(200 * time.Millisecond) ch2 <- "from ch2" }() // Wait for first available message for i := 0; i < 2; i++ { select { case msg1 := <-ch1: fmt.Println("Received:", msg1) case msg2 := <-ch2: fmt.Println("Received:", msg2) case <-time.After(500 * time.Millisecond): fmt.Println("Timeout!") } } } ``` > **Go Philosophy: CSP** > > Go follows the CSP (Communicating Sequential Processes) model: "Don't communicate by sharing memory; share memory by communicating." Channels prevent race conditions. ## Testing in Go Go includes a minimalist but effective testing framework. Test files end with `_test.go`. ```go // calculator.go package calculator func Add(a, b int) int { return a + b } func Divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } ``` ```go // calculator_test.go package calculator import ( "testing" ) // Basic test func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5 if result != expected { t.Errorf("Add(2, 3) = %d; want %d", result, expected) } } // Table-driven tests (recommended pattern) func TestAddTableDriven(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -2, -3, -5}, {"zero", 0, 0, 0}, {"mixed", -5, 10, 5}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } } // Error test func TestDivideByZero(t *testing.T) { _, err := Divide(10, 0) if err == nil { t.Error("Expected error for division by zero") } } // Benchmark func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(100, 200) } } ``` Tests run with `go test ./...` and benchmarks with `go test -bench=.`. ## HTTP: Minimalist Web Server Go excels at creating high-performance HTTP servers using its standard library. ```go // server.go package main import ( "encoding/json" "log" "net/http" ) type User struct { ID int `json:"id"` Name string `json:"name"` } func main() { // Simple route http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, Go!")) }) // JSON route http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { users := []User{ {ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) }) // Route with method http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": w.Write([]byte("Get user")) case "POST": w.Write([]byte("Create user")) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }) log.Println("Server starting on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } ``` This server handles thousands of concurrent connections thanks to goroutines. Each request is automatically processed in its own goroutine. ## Conclusion Go offers a pragmatic approach to backend development: simple syntax, fast compilation, native concurrency, and excellent tooling. For Java or Python developers, the transition requires accepting some different conventions (explicit error handling, limited generics before Go 1.18), but the performance and maintainability benefits are immediate. ### Checklist for Getting Started - ✅ Install Go via the official site or package manager - ✅ Master the commands `go build`, `go run`, `go test`, `go fmt` - ✅ Understand the difference between slices and arrays - ✅ Adopt the `if err != nil` pattern for error handling - ✅ Use goroutines and channels for concurrency - ✅ Write table-driven tests with the `testing` package The Go ecosystem is mature with popular frameworks like Gin, Echo, and Fiber for web development, and tools like Cobra for CLIs. With these solid foundations, exploring advanced topics like generics (Go 1.18+), the context package, and concurrency patterns becomes accessible. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/go/go-basics-java-python-developers