cover_image

从Java到Go:“哭笑不得”

朱进 腾讯云开发者
2024年12月24日 04:01
图片
图片

👉目录


1 语法的“简约”与“简陋”

2 类型系统的“死板”和“灵活”

3 编程语言的‘面向对象’大比拼

4 指针的“直白”表达

5 处理错误的“优雅”与“粗暴”

6 并发的“轻松”与“混乱”

7 反射的“强大”与“局限”

8 社区文化的“开放”与“封闭”

9 结语




今天我们要聊聊一个有趣的话题——从 Java 转到 Go 的那些不适应、需要学习的地方。

关注腾讯云开发者,一手技术干货提前解锁👇

//////////


12 月 26 日晚 7:30,腾讯云开发者视频号「鹅厂程序员面对面」直播间,我们邀请了作者来为你分析资金视角看支付系统架构,预约观看有机会抢鹅厂周边好礼!







01



语法的“简约”与“简陋”


首先,Java 的语法是相对严谨的,类、接口、继承、抽象……一大堆的关键字让你在写代码时感觉像是在参加一场语法考试。而 Go 的语法则是简约得让人惊讶。你会发现, Go 里没有类的概念,只有结构体(struct)和接口(interface)。这让很多 Java 的 coder在初入 Go 时感到无所适从...


例子:在 Java 中,你可能习惯于写出这样的代码:


public class Dog {    private String name;
public Dog(String name) { this.name = name; }
public void bark() { System.out.println(name + " says Woof!"); }}


而在 Go 中,你可能会写成:


type Dog struct {    Name string}
func (d Dog) Bark() { fmt.Println(d.Name + " says Woof!")}


乍一看,Go 的代码似乎简单了很多,但对于习惯了 Java 的开发者来说,少了很多“保护”的感觉,心里总是有点不安。你可能会想:“我真的可以不使用构造函数吗?这样会不会出问题?”




02



类型系统的“死板和“灵活


   2.1 静态类型 vs 动态类型


  • Java:

Java 是一种静态类型语言,所有变量的类型在编译时就必须确定。类型检查在编译阶段进行,这有助于捕获类型错误。例如:


int number = 10; // number 的类型在编译时确定为 int


  • Go:

Go 也是一种静态类型语言,但它支持类型推断。变量的类型可以在声明时省略,编译器会根据赋值自动推断类型。例如:


number := 10 // number 的类型在编译时被推断为 int


   2.2 类型声明


  • Java:

Java 使用显式的类型声明,所有变量和函数参数都需要明确指定类型。例如:


String name = "Alice";


  • Go:

Go 支持简洁的类型声明,可以使用 := 语法进行简化,同时也可以使用 var 关键字进行显式声明。例如:


var name string = "Alice"age := 30 // 类型推断为 int


   2.3 类型转换


  • Java:

Java 支持显式和隐式类型转换,但需要注意类型兼容性,特别是在对象之间的转换时。例如:


double d = 10.5;int i = (int) d; // 显式转换
byte b = 10;int i = b; // 隐式转换, byte 转换为 int


  • Go:

Go 也支持显式类型转换,但不支持隐式转换,必须明确指定转换。例如:


var d float64 = 10.5var i int = int(d) // 显式转换




03



编程语言的‘面向对象’大比拼


   3.1 对象


  • Java:
在 Java 的世界里,对象就像是一个高档餐厅的顾客。你得先用 new 关键字点菜(创建对象),然后才能享用美味的“类”大餐。


Person person = new Person("Alice", 30); // 点了一位名叫 Alice 的顾客


  • go:

在 Go 的世界里,对象就像是一个随意的 BBQ 聚会。你可以直接用结构体字面量来“烤”出一个对象,简单又直接。


person := Person{Name: "Alice", Age: 30} // 直接上桌,没那么多讲究


   3.2 类型


  • Java:
Java 是个严谨的家伙,所有的变量都得在编译时穿上合适的“类型”衣服。你不能让一个 int 突然变成 String,这就像让一只猫穿上狗的衣服一样不合适。

int number = 10; // 这就是我的类型,没得商量


  • Go:

Go 则是个随性的人,支持类型推断。你可以用 := 直接声明,像是在派对上随便找个饮料喝。


text := "Hello" // 喝什么都行,随便来


   3.3 接口


Java: 在 Java 的世界里,接口就像是一个严格的俱乐部,只有那些愿意遵守规则的类才能加入。你得用 implements 关键字来申请入会。


  • Go: 

接口是一个方法集,任何实现了这些方法的类型都自动满足该接口。接口不需要显式声明实现。


type Greeter interface {    Greet()}
type Dog struct{}
func (d Dog) Greet() { fmt.Println("Woof!")}


  • Java:

接口定义了一组方法,类需要使用 implements 关键字来实现接口。Java 接口可以包含默认方法和静态方法。


interface Greeter {    void greet();}
class Dog implements Greeter { public void greet() { System.out.println("Woof!"); }}


   3.4 继承


  • Java:

Java 是个传统的家族,支持单继承。你只能有一个父母(父类),但可以有很多朋友(接口)。


class Dog extends Animal { /* 继承父母的特质 */ }


  • Go:

Go 则是个现代家庭,支持组合。你可以把多个结构体“嵌入”到一个结构体中,像是把不同的特质混合在一起。


type Dog struct { Animal } // 组合而成,像是拼图


   3.5 多态


  • Java:
多态就像是一个变色龙,能够根据上下文改变自己的颜色。你可以用父类的引用指向子类的对象,真是个神奇的家伙。

Animal animal = new Dog();

  • Go:
的多态则像是一个万花筒,任何实现了接口的类型都可以被当作接口使用,五彩斑斓。

var greeter Greeter = Dog{} // 万花筒

   3.6 总结


Java: 你得穿好衣服,遵守规则,才能在这个严谨的世界里生存。

Go: 你可以随意穿搭,随性而为,享受生活的乐趣。


特性GoJava
类和对象

类的定义Go 没有类的概念,使用结构体(struct)来定义数据类型。Java 使用类(class)来定义对象的蓝图。
对象的创建使用结构体字面量或 new 关键字创建对象。使用 new 关键字创建对象。
继承Go 不支持传统的继承,但支持组合(embedding)。Java 支持单继承和接口的多继承。
构造函数Go 没有构造函数的概念,通常使用工厂函数来创建对象。Java 支持构造函数,可以重载。
方法方法可以定义在结构体上,且可以接收指针或值接收者。方法定义在类中,使用 this 关键字访问实例变量。
接口

接口的定义使用 interface 关键字定义接口,方法不需要实现。使用 interface 关键字定义接口,类需要实现接口中的方法。
接口实现类型只需实现接口中的方法,自动满足接口,无需显式声明。类使用 implements 关键字显式实现接口。
多重接口实现一个类型可以实现多个接口。一个类可以实现多个接口。
多态

多态通过接口实现多态,使用接口类型的变量可以存储任何实现该接口的类型。通过方法重载和方法覆盖实现多态。



04



指针的“直白”表达


   4.1 Java:引用的温柔包围


在 Java 中,你与对象的交互是通过引用来完成的,指针的概念被巧妙地隐藏了起来。你可以安心地使用对象,而不必担心指针的复杂性。

   4.2 Go:指针的“直率”邀请


而在 Go 中,指针就像是个直率的朋友,直接告诉你:“嘿,我指向的是哪里!”你需要用 * 和 **& **来操作指针。刚开始时,你可能会觉得自己像是在解谜,搞不清楚到底在指向什么。

value := 10ptr := &value // ptr 是 value 的指针*ptr = 20     // 修改了 value 的值



05



处理错误的“优雅”与“粗暴”


在 Java 中,异常处理是一个相对优雅的过程。你可以使用 try-catch-finally 结构来捕获和处理异常,代码看起来也很整洁。然而,在 Go 中,错误处理的方式让很多 Java 开发者感到“粗暴”。

例子:在 Java 中,你可能会这样处理异常:

try {    // 可能抛出异常的代码} catch (Exception e) {    e.printStackTrace();}

而在 Go 中,你需要在每个可能出错的地方手动检查错误:

result, err := someFunction()if err != nil {    log.Fatal(err)}

这种方式虽然让错误处理变得显式,但对于习惯了 Java 的开发者来说,心里总是想着:“难道就不能像 Java 一样优雅地处理错误吗?”随着代码的复杂性增加,错误处理的代码也会随之增多,导致代码的可读性下降。


好的,让我们更详细地探讨一下 Go 的并发模型、Go Modules 和 vendor 目录的使用,并通过具体的例子来说明它们在实际运行中的表现。




06



并发的“轻松”与“混乱”


Go 的并发模型是其一大亮点,goroutine 和 channel 的使用让并发编程变得轻松。goroutine 是一种轻量级的线程,Go 运行时会自动管理它们的调度。相比于 Java 的线程,goroutine 的创建和销毁开销更小,能够更高效地利用系统资源。

   6.1 Go:指针的“直率”邀请


假设我们要并发地下载多个网页并打印它们的内容。在 Java 中,你可能会使用线程池来实现,而在 Go 中,你可以使用 goroutine 和 channel 来实现。


Java 示例:


// 导入所需的Java库import java.io.BufferedReader; // 用于读取输入流import java.io.InputStreamReader; // 用于将字节流转换为字符流import java.net.HttpURLConnection; // 用于处理HTTP连接import java.net.URL; // 用于处理URL地址import java.util.concurrent.ExecutorService; // 用于管理线程池import java.util.concurrent.Executors; // 用于创建线程池
// 定义一个名为WebDownloader的公共类public class WebDownloader { // 定义主方法,程序的入口点 public static void main(String[] args) { // 定义一个字符串数组,包含三个URL地址 String[] urls = {" http://example.com" , " http://example.org" , " http://example.net" }; // 创建一个固定大小的线程池,大小为3 ExecutorService executor = Executors.newFixedThreadPool(3); // 遍历URL数组 for (String url : urls) { // 提交一个任务到线程池,任务是一个lambda表达式 executor.submit(() -> { try { // 打开一个HTTP连接到指定的URL HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); // 创建一个BufferedReader对象,用于读取连接的输入流 BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); // 定义一个字符串变量,用于存储从输入流中读取的每一行数据 String inputLine; // 循环读取输入流中的每一行数据,直到没有数据为止 while ((inputLine = in.readLine()) != null) { // 打印读取到的每一行数据 System.out.println(inputLine); } // 关闭BufferedReader对象,释放资源 in.close(); } catch (Exception e) { // 如果发生异常,打印异常堆栈跟踪信息 e.printStackTrace(); } }); } // 关闭线程池,不再接受新的任务,等待所有已提交的任务完成 executor.shutdown(); }}

这段 Java 代码的功能是并发地发送 HTTP GET 请求到指定的 URL 列表,并打印每个 URL 的响应体内容。使用了 sync.WaitGroup 来确保所有 goroutine 都完成后,主函数才退出。

Go 示例:

// 导入需要的包package main
import ( "fmt" // 用于格式化输出 "io/ioutil" // 用于读取响应体 "net/http" // 用于发送HTTP请求 "sync" // 用于同步goroutine)
// 主函数func main() { // 定义一个字符串切片,包含需要请求的URL urls := []string{" http://example.com" , " http://example.org" , " http://example.net" }
// 定义一个WaitGroup,用于等待所有goroutine完成 var wg sync.WaitGroup
// 遍历URL列表 for _, url := range urls { // 为每个URL增加一个计数器 wg.Add(1)
// 启动一个新的goroutine来处理请求 go func(url string) { // 在goroutine结束时调用Done(),减少WaitGroup的计数器 defer wg.Done()
// 发送HTTP GET请求 resp, err := http.Get(url) if err != nil { // 如果请求出错,打印错误信息并返回 fmt.Println(err) return }
// 读取响应体的内容 body, _ := ioutil.ReadAll(resp.Body)
// 打印响应体的内容 fmt.Println(string(body))
// 关闭响应体 resp.Body.Close() }(url) // 将当前URL传递给goroutine }
// 等待所有goroutine完成 wg.Wait()}

在这个 Go 示例中,我们使用了 sync.WaitGroup 来等待所有的 goroutine 完成。每个 goroutine 都会并发地下载网页内容并打印出来。Go 的并发模型让这个过程变得非常简单和直观。



07



反射的“强大”与“局限”


在 Java 中,反射是一个强大的特性,可以让你在运行时检查和操作对象的属性和方法。然而,在 Go 中,反射虽然也存在,但使用起来却相对复杂,并且有很多限制。

例子:在 Java 中,你可以轻松地使用反射来获取类的信息:

Class<?> clazz = Dog.class;Method[] methods = clazz.getDeclaredMethods();

而在 Go 中,反射的使用需要导入 reflect 包,并且需要更多的代码来实现相同的功能:

import "reflect"
t := reflect.TypeOf(Dog{})methods := t.NumMethod()

对于习惯了 Java 的开发者来说,Go 的反射机制可能会让人感到不够直观,尤其是在需要动态处理类型时,反射的局限性可能会让你感到沮丧。




08



社区文化的“开放”与“封闭”


最后,Java 和 Go 的社区文化也有很大不同。Java 社区相对成熟,很多开发者习惯于在 Stack Overflow 和各种论坛上寻求帮助。而 Go 的社区则更加开放,很多开发者习惯于在 GitHub 上直接参与项目的开发和维护。


这让很多 Java 开发者在刚接触 Go 时,可能会感到有些不适应,尤其是在寻求帮助时,可能会觉得“我该去哪里问问题呢?”而且,Go 的文档和社区资源虽然丰富,但有时也会让人感到信息过载,不知道从何入手。




09



结语


总的来说,从 Java 转到 Go 的过程就像是一场冒险,有惊喜也有收获,欢迎大家分享你们在从 Java 转到 Go 过程中遇到的趣事和挑战,我们一起交流学习!


-End-
原创作者|朱进

图片


你对 Java 和 Go 这两个语言有什么看法?欢迎评论留言。我们将选取1则优质的评论,送出腾讯云定制文件袋套装1个(见下图)。12月31日中午12点开奖。

图片


📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~


图片

(长按图片立即扫码)


图片
图片

图片

图片

图片

腾讯技术人原创集 · 目录
上一篇程序员必备!一文讲透晋升答辩!下一篇架构实践:同时支持单体、微服务,单台服务器还能支撑十几万用户?
继续滑动看下一个
腾讯云开发者
向上滑动看下一个