随着 Go 语言的流行,很多公司的技术栈在往 Go 上转,但很多招进来的后端开发工程师都是 Java 技术栈,然后在工作中边学边上手。
那么 Java 程序员要想极速上手 Go,应该从哪些方面入手呢?
对于已经有一定基础的 Java 工程师,可以思考自己以前用 Java 编程时,最常使用的语言特性,列一个清单出来。然后按照这个清单,去学习 Go 语言的对应实现方式,这样能够有针对性的的学习,有的放矢。
下面是我使用 Java 进行日常工作中经常使用的5个编程要点,我会介绍这些要点对应的 Go 实现,仅供大家参考。
1. 并发编程(Concurrency)
Go语言通过goroutine和channel提供了轻量级的并发编程模型,与Java的线程模型相比更加简洁和高效。下面是一个简单的并发示例:
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个 go routine
go sayHello("World")
// 主 goroutine 打印 Hello
sayHello("Gopher")
}
func sayHello(name string) {
for i := 0; i < 5; i++ {
fmt.Println("Hello", name)
time.Sleep(time.Second)
}
}
执行上述 go 代码,打印输出:
笔者另外一篇文章,曾经详细介绍过 Go 的并发编程,大家可以移步我这篇腾讯云社区文章:
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
2. 内置并发安全的Map(Built-in concurrency-safe Map)
在Go语言中,内置了并发安全的map类型(sync.Map),可以在多个goroutine中安全地读写数据,而无需额外的锁。以下是一个使用sync.Map的示例:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 写入键值对
m.Store("key", "value")
// 读取键对应的值
value, ok := m.Load("key")
if ok {
fmt.Println(value)
}
}
执行上述代码,最后打印 value
:
这段代码是一个简单的Go语言程序,它演示了如何使用sync.Map进行并发安全的键值对操作。下面是逐行解释这段代码的含义:
package main
:声明这个文件属于main包,表明这是一个可执行程序的入口文件。import (
:引入需要使用的外部包,这里使用了"fmt"和"sync"包。"fmt"
:引入了用于格式化输出的标准库包。"sync"
:引入了用于同步操作的标准库包。func main() {
:定义了程序的主函数。var m sync.Map
:声明了一个变量m,类型为sync.Map。sync.Map是Go语言提供的一种并发安全的map类型,可以在多个goroutine中安全地读写数据,而无需额外的锁。m.Store("key", "value")
:向map m中存储键值对,键为"key",值为"value"。value, ok := m.Load("key")
:从map m中读取键为"key"的值,如果键存在,则将值赋给变量value,并将ok设置为true;如果键不存在,则value为nil,ok为false。这里使用了多重赋值的方式。if ok {
:判断变量ok是否为true,如果为true,说明键存在。fmt.Println(value)
:打印键"key"对应的值value。}
:if语句的结束。
整个程序的逻辑很简单,就是先向sync.Map中存储了一个键值对,然后再从中读取出来并打印出对应的值。由于sync.Map是并发安全的,所以可以在多个goroutine中安全地进行这些操作。
3. 错误处理(Error Handling)
Go语言采用显式的错误处理机制,通过返回error类型来传递错误信息,而不是Java中的异常。下面是一个简单的错误处理示例:
package main
import (
"errors"
"fmt"
)
func divide(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
执行之后的输出:
以下是对代码逐行解释的含义:
import (
"errors"
"fmt"
)
这里使用了import
关键字引入了两个标准库包。errors
包用于创建和处理错误信息,fmt
包用于格式化输出。
func divide(x, y int) (int, error) {
这行代码定义了一个名为divide
的函数。这个函数接受两个int
类型的参数x
和y
,并返回两个值,第一个值是int
类型的商,第二个值是error
类型的错误信息(如果有错误的话)。
if y == 0 {
return 0, errors.New("division by zero")
}
在函数内部,首先检查y
是否为零。如果y
为零,就会调用errors.New
函数创建一个新的错误,其中包含字符串"division by zero"
,然后返回0和该错误。
return x / y, nil
如果y
不为零,则返回x / y
作为商,并且返回nil
表示没有错误发生。
func main() {
这行代码定义了程序的主函数。
result, err := divide(10, 0)
在主函数中,调用了divide
函数,传入了参数10和0。result
接收到divide
函数返回的商,err
接收到可能返回的错误。
if err != nil {
检查err
是否不为nil
,如果不为nil
,说明函数调用过程中发生了错误。
fmt.Println("Error:", err)
如果有错误发生,使用fmt.Println
打印错误信息。
return
然后通过return
语句结束函数执行。
fmt.Println("Result:", result)
如果没有发生错误,则打印商的结果。
整体而言,这段代码展示了一个简单的除法函数,并演示了如何处理可能的错误情况。
4. defer和panic/recover(defer and panic/recover)
Go语言提供了defer关键字用于延迟执行函数调用,以及panic和recover机制用于处理异常。以下是一个使用defer和panic/recover的示例:
package main
import "fmt"
func recoverName() {
if r := recover(); r != nil {
fmt.Println("recovered from", r)
}
}
func fullName(firstName *string, lastName *string) {
defer recoverName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
defer fmt.Println("deferred call in main")
firstName := "John"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
执行之后,打印的输出结果:
这段代码演示了在Go语言中如何使用defer
、panic
和recover
来处理运行时错误。具体来说,它包含了以下几个关键点:
recoverName()
函数定义了一个延迟函数,它通过调用recover()
函数来恢复从panic
状态中返回的错误信息。如果recover()
函数返回的值不为nil
,则说明有panic
发生,并且在该函数中进行了错误恢复处理。fullName()
函数展示了如何在运行时可能出现错误的情况下使用panic
来中断程序执行,并在发生错误时抛出错误信息。在这个函数中,首先使用defer
关键字来延迟执行recoverName()
函数,以确保无论是否发生panic
,都能在函数退出时执行恢复处理。然后,通过检查传入的指针是否为nil
来判断是否存在运行时错误,如果存在错误,就会触发panic
,并且程序会跳转到最近的defer
语句执行。main()
函数是程序的入口点。在这个函数中,通过使用defer
语句来延迟执行fmt.Println("deferred call in main")
,确保该语句会在函数退出时被执行。然后定义了一个firstName
变量,并调用了fullName()
函数,将firstName
的地址和一个nil
指针作为参数传递给该函数。由于lastName
参数为nil
,因此会触发panic
,并且程序会跳转到fullName()
函数中执行错误处理。最后,fmt.Println("returned normally from main")
语句会在main()
函数正常返回时执行,但由于在调用fullName()
函数时已经发生了panic
,因此该语句不会被打印出来。
5. 结构体(Structs)和接口(Interfaces)
Go语言中的结构体和接口是非常灵活和强大的,可以有效地组织代码和实现多态性。下面是一个使用结构体和接口的示例:
package main
import (
"fmt"
)
type Shape interface {
Area() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func printArea(s Shape) {
fmt.Println("Area:", s.Area())
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
circle := Circle{Radius: 4}
printArea(rect)
printArea(circle)
}
执行之后的代码输出:
这段代码演示了如何在Go语言中使用接口和结构体来实现多态性。具体来说,它包含了以下几个关键点:
Shape
接口:定义了一个Shape
接口,该接口包含一个Area()
方法,用于计算形状的面积。任何实现了Area()
方法的类型都可以被视为Shape
接口的实现者。Rectangle
结构体:定义了一个Rectangle
结构体,包含了Width
和Height
两个字段,分别表示矩形的宽度和高度。此外,还实现了Area()
方法,用于计算矩形的面积。Circle
结构体:定义了一个Circle
结构体,包含了Radius
字段,表示圆的半径。同样,它也实现了Area()
方法,用于计算圆的面积。printArea()
函数:定义了一个printArea()
函数,接受一个Shape
类型的参数,并调用其Area()
方法来打印形状的面积。main()
函数:程序的入口点。在这个函数中,创建了一个Rectangle
类型的变量rect
和一个Circle
类型的变量circle
,并分别传递给printArea()
函数进行面积打印。由于Rectangle
和Circle
类型都实现了Shape
接口的Area()
方法,因此它们都可以作为printArea()
函数的参数,展示了多态性的特性。
以上只是五个笔者工作中最经常使用到的 Go 特性分享,祝各位 Java 程序员的 Go 转型之路一切顺利。