从Java到Golang入门杂谈

简介: 作为一名Java老兵,入门Golang的一些体验和思考记录,本篇只是一些零碎的记录,不打算写成一个Golang入门指南,如果需要入门指南请参考其他文档或者书籍。

一句话总结:Java是一门很好的语言,Golang是一门很有意思的语言。

理念上的区别

Java诞生于企业信息化年代,那个时候软件工程师还是一份很严肃的职业,所以Java的设计从骨子里透着严谨和认真,并且力求完备周全,无论是虚拟机的规范,还是API文档都可以看出来在Gasling的带领下,Java团队构建了一个非常严谨而完备的技术平台,让Java Developer能够在不需要了解太多的底层细节就能写出安全可靠的代码,相比之前的C/C++大幅度降低了技术风险,符合普通人直觉的OOP设计以及Design Pattern的总结,也让很多初级程序员能够快速上手,把业务需求转换为代码实现,从而带来了整个Java世界的繁荣。
但是到了互联网时代之后,Java的很多设计让很多代码变得冗长而无趣,对于高水平程序员来说亟需一种表达和控制能力更强,也更简洁的语言,从而更好地提高开发的效率。所以Google结合了C和脚本语言和现代计算机语言理论发展,带来了Golang,由于有了前人的经验和踩过的坑,所以Golang从一开始就秉承着不一样的理念。从Golang Proverbs我们就可以看出来,其与Java有着一些有趣的区别。

Don't communicate by sharing memory, share memory by communicating.

首先Golang强调了通讯而非共享,在Java里面我们遵从的是OOP封装数据暴露方法的原则,但是对象是可以传递的,而对象(状态)实质上就是一片内存里的数据,而对象传递和引用被滥用之后,一个对象可能会被很多地方引用,并且都有可能改变状态,也就很容易带来系统内的逻辑耦合。而Golang从设计之初就希望通过通讯的方式最小化共享的状态,不论是通过channel来传递一个小的消息,还是最小化interface的实现,都透着让程序员减少关联和引用的画外音。

Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.

其次为了避免大家在Code Convention上浪费时间争论括号应该放在哪儿,用空格还是用tab,Golang直接规定了标准格式,看起来有点霸道,但是保证统一的风格之后,确实提升了代码的可读性,也简化了代码检查工具的负担,因为只要用官方工具即可。从这一条我们可以看出来Golang的设计者是实用主义者,不愿意在一些意义不大的地方浪费时间,而是希望程序员能够专注于解决问题。

A little copying is better than a little dependency.

这一条有点反直觉,特别是对于一些动辄复用的Java程序员来说应该是很难受的。Golang里面并不鼓励为了复用而复用,潜台词是:“如果只是简单的几行代码,那么直接复制过来用即可,不要想那么多”。从过去的Java从业经历来看,这一条太赞了,有太多的xxx.utils和代码规约为了一两行代码的复用带来额外学习和理解成本,引入的依赖导致为了一根绳子牵出一头牛,导致产品越来越臃肿而难以改变。

简单直接

还有一条Golang proverbs没有提,但是我觉得也是Golang的一个设计理念,就是简单直接,能够直接给你的,就不要包一层再给你,程序员自己需要对代码负责。比如struct的exported fields可以直接访问,而不需要getter/setter包一层。而由调用者决定接口,而非提供者定义接口的做法,让我们可以避免搞出一堆IXxx,直接写实现即可。

实用主义而非完美主义

实用主义在Golang的设计中随处可见,不再追求完美之后就给大家带来了一些有争议但是也非常有意思的设计,当然可能也会害死强迫症患者。
比如Golang的命名规约中,大写字母开头是表示exported,大致相当于Java里面的public,但是问题是内置函数全部都是小写,为啥内置函数就可以例外呢?还有slice的append()方法,必须写成s = append(s, ...),为啥不能给个类似s.append(...)的语法糖呢?

一些Java程序员容易踩的坑

nil是有类型的

最开始的时候,把nil当成了Java的null,但是很快就被打脸,比如下面的代码:

// custom error type
type myErr struct{
   
  // fields omitted
}

func err() *myErr {
   
  return nil
}

func checkErr(err error) bool {
   
  return err == nil
}

这里checkErr(err())会返回false,原因在于err()返回的是*myErr类型的nil,而后面checkErr()判断的是error类型的nil,显然是对不上的。

slice和map

slicemap的实际存储是一个引用,但不是一个指针,这样也很容易让初学者迷糊。比如s = append(s, ...),这个调用过后,可能s还是指向原来的存储,也可能不是,取决于原来的存储容量是否能承载新加入的元素。这样就有了类似下面的代码片段(来自Go 101):

a := [...]int{
   0, 1, 2, 3}
x := a[:1]
y := a[2:]
x = append(x, y...)
x = append(x, y...)
fmt.Println(a, x)

初学者肯定想不到最后打印的是什么内容,顺便说一下Go 101是一个非常好的学习Golang的资源,值得一读。

还有就是对于未初始化的map,读是没有问题的,写就会报错,比如:

var m map[string]string // nil map
fmt.Println(m["foo"]) // no error
m["foo"] = "bar" // panic

而在Java里面,类似的代码:

Map m = null;
m.get("foo"); // NullPointerException

channel不能被close两次

一般而言,我们会认为一个资源可以被close多次,除了第一次以外其他都相当于无操作(no op),但是在golang里面,最基本的语言元素channel竟然只能close一次,之后再次close就会panic。

comparable不能比大小

Golang内置的comparable接口,只能用== !=来比较相等或者不等,如果要用> <来比较大小的话,需要的是ordered接口,这个让对于习惯Java的Comparable的人有点迷糊,同时也更加深刻地理解了Golang最小化接口定义的决心。

pprof给出来的数据看不懂

一开始我把pprof获得的heap信息当成了Java heap dump来看,但是发现完全看不懂,后来发现pprof给出来的是内存分配的调用链路,而不是当前的object关系图,所以从Java过来的同学需要好好适应一下。

type embedding不是继承

type embedding的机制,在用起来的时候有点像是继承,但是千万不要被表象所迷惑,因为type embedding不是继承,而且差异非常大。比如如下代码:

type Base struct {
   
}

func (b *Base) m1() {
   }
func (b *Base) m2() {
   
  b.m1()
}


type Child struct {
   
  *Base
}

func (c *Child) m1() {
   }

从外面调用的角度来看,Childm1()能够直接被使用,但是调用m2()的时候,里面实际调用的是m1(),这个对于从Golang入手的人来说非常好理解,但对于习惯了OOP的人来说非常反直觉。实际上所有围绕着Base类型的方法,当出现引用方法定义中的类this指针的那个变量时,引用的都是那个具体的类型,不会有重载。外部使用只不过是编译器给加了一个语法糖而已。也就是说:

var cc Child
cc.m2()
cc.Base.m2()

上面两个方法调用是完全等价的,前面那个只不过是编译器帮你省了一个变量名而已。

文档在哪里?

一开始上手的时候,我发现一个非常麻烦的问题,找不到文档,或者找到了也语焉不详,看不太明白。因为Golang的文档相比Java过于简洁了,官方库还好,很多非常流行的第三方库也是寥寥数语,而且缺乏概括性的介绍,如果单看文档很难了解如何使用,所以开始的时候还是挺怀念java doc的。
熟悉了之后发现还好,要了解代码的逻辑,最好的方法还是直接看源代码,因为golang的所有库都是提供源代码的,所以直接进去看就好了,很多细节直接读源代码很快就明白了,如果看文档或者搜索都是很难找到正确答案的。

Golang里舒服的设计

Golang有很多让程序员舒服的设计,这里主要讲一下Java里很难看到的。

简单的赋值表达

有点类似JavaScript,在Golang里面可以直接对结构进行赋值,比如:

type Foo struct{
   
  field1 string
  field2 int
}

foo := Foo{
   
  field1: "bar",
  field2: 1234,
}

相比在Java里面需要一堆的setter/getter给大的数据结构赋值或者复制很多字段,来得轻松了很多。
另外map的读写也非常简洁,不需要大量的get() set()让代码变得赏心悦目。

类型别名

Golang不允许跨包给别的类型添加方法,但是提供了type alias这种办法允许程序员在别的数据类型基础上重新定义方法,这个设计大幅度避免了为了扩展一个已有结构的功能,而不得不加入一个子类的冗余代码。即使是内置的数据类型,依然可以做type alias,golang自带的库里就有很多直接给基本类型设置type alias来实现不同逻辑功能的例子。比如我们需要一个64 bits的timestamp,可以这么设计:

type Timestamp int64
func (t Timestamp) String() string {
   
  return fmt.Sprintf("%d", t)
}
func (t *Timestamp) AddSeconds(secs int) {
   
  *t = (*t) + int64(secs)
}

因为type alias也是一个新的类型,编译器不会把这个类型当作原来的类型(除非强转),所以样调用方可以很方便地使用这个新的Timestamp类型,并且不用担心引入一些因为忽略了实际类型导致的bug。

包管理

Golang是一门互联网时代的语言,所以任何一个Golang的包,都是直接从官方repository里面下到的,不需要额外引入类似maven这种仓库,这样好处是非常方便。但是也需要注意golang的非官方包也出现在准官方repository里面,但是并不意味着有官方的质量保证和承诺,我们需要自行研究才能知道这些引入的第三方库是否合适。

辅助工具

除了语言以外,Golang提供了很多开箱即用的工具,比如golint, gofmt,pprof等,这些工具大大简化了新手上路的负担,识别了很多基础错误和问题。

打包成单个执行文件

这个功能相比于Java的CLASSPATH方便太多了,不会出现在这个环境里能正确执行,换个环境就不行的问题,原因在于CLASSPATH可以随意配置,而单个可执行文件已经包含了所有依赖前不会变更,避免了很多环境配置带来的问题。相比python的依赖管理更是强出100条街。

一点感想

Golang设计之初就定位为非OOP的语言,所以所有的OOP相关的习惯在Golang都不适用,甚至会带来bug和问题,所以从Java到Golang首先要提醒自己的,就是忘记OOP相关的概念和代码模式,切换成我要直接操作一个数据结构,该怎么写代码这个思维模式。其次就是需要忘记各种接口定义和封装,特别是各种IXxx类型的接口定义,也不要写一堆的utils,因为Golang其实没有办法阻止我们写出类似Java般冗余的代码,加上一堆的额外接口和复杂的结构之后,只会让代码变得非常难看。还有一点,package的命名非常重要,既要简单易懂,又不能占用用户常用的命名,非常考验程序员(特别是中国程序员)的词汇量和对英语的理解。

未完待续

目录
相关文章
|
10天前
|
数据采集 Java
selenium+java入门demo
selenium+java入门demo
23 4
|
10天前
|
存储 Java 开发者
Java数据类型:从入门到精通,你不得不看的“宝典”
【6月更文挑战第13天】Java凭借其跨平台能力和丰富的类库深受开发者喜爱。学习Java首先需掌握数据类型,包括基本类型(整型、浮点型、字符型、布尔型)和引用类型(类、接口、数组)。基本类型占用固定内存,值不可变;引用类型存储对象地址。通过示例代码展示了如何声明和使用这些类型。此外,Java的封装类如Integer等提供了操作便利。理解数据类型对于程序设计至关重要,是进一步学习Java高级特性的基础。开始你的Java编程之旅,不断实践,你将在Java世界中不断进步。
|
16天前
|
存储 Java
Java入门——数据类型、自动类型转换、强制类型转换
Java入门——数据类型、自动类型转换、强制类型转换
13 2
|
16天前
|
Java 程序员
Java入门——JDK的组成、Java的跨平台、Path环境变量配置、Java_home环境变量
Java入门——JDK的组成、Java的跨平台、Path环境变量配置、Java_home环境变量
15 3
Java入门——JDK的组成、Java的跨平台、Path环境变量配置、Java_home环境变量
|
2天前
|
网络协议 安全 Java
Java网络编程入门涉及TCP/IP协议理解与Socket通信。
【6月更文挑战第21天】Java网络编程入门涉及TCP/IP协议理解与Socket通信。TCP/IP协议包括应用层、传输层、网络层和数据链路层。使用Java的`ServerSocket`和`Socket`类,服务器监听端口,接受客户端连接,而客户端连接指定服务器并交换数据。基础示例展示如何创建服务器和发送消息。进阶可涉及多线程、NIO和安全传输。学习这些基础知识能助你构建网络应用。
10 1
|
3天前
|
Java 开发者
告别单线程时代!Java 多线程入门:选继承 Thread 还是 Runnable?
【6月更文挑战第19天】在Java中,面对多任务需求时,开发者可以选择继承`Thread`或实现`Runnable`接口来创建线程。`Thread`继承直接但限制了单继承,而`Runnable`接口提供多实现的灵活性和资源共享。多线程能提升CPU利用率,适用于并发处理和提高响应速度,如在网络服务器中并发处理请求,增强程序性能。不论是选择哪种方式,都是迈向高效编程的重要一步。
|
5天前
|
存储 安全 Java
Java Queue:从入门到精通,一篇文章就够了!
【6月更文挑战第18天】Java集合框架中的队列Queue遵循FIFO原则,用于存储和管理元素。从创建队列(如LinkedList示例)到移除元素(remove和poll方法),再到不同实现类(如ArrayDeque和ConcurrentLinkedQueue),队列在多线程、任务调度等场景中广泛应用。自定义队列如LimitedQueue展示如何限制容量。了解并熟练使用队列能提升程序性能和可读性。队列,是高效编程的关键工具。
|
5天前
|
安全 Java 索引
Java List:从入门到精通,一篇文章就够了!
【6月更文挑战第17天】Java List是有序元素集合,支持索引访问、添加、删除和修改。从ArrayList、LinkedList到Vector,各种实现满足不同场景需求。使用add()添加元素,get()获取,set()修改,remove()删除。遍历可用for-each或Iterator,subList()创建子集。注意线程安全,可选synchronizedList()、Vector或CopyOnWriteArrayList。理解List的基本操作和特性,能提升编程效率。
|
12天前
|
Kubernetes Go 云计算
Golang 入门技术文档
**Golang 技术文档摘要:** Golang,由Google开发,是一种静态强类型、编译型语言,广泛应用于云计算、网络编程和分布式系统。本文档介绍了Golang的基础和特性,包括安装配置、 HelloWorld 示例、基本语法,如变量推导、函数多返回值和并发编程(goroutine、channel)。Golang的并发模型基于轻量级goroutine和channel,支持高效并发处理。此外,文档还提及了接口和多态性,展示了如何使用接口实现类型间的交互。Golang在Docker、Kubernetes等项目中得到应用,适用于后端服务开发。【6月更文挑战第9天】
18 1
|
16天前
|
IDE Java 项目管理
Java入门——Intellij IDEA简介、使用IDEA开发程序、IDEA常用快捷键、IDEA其他操作
Java入门——Intellij IDEA简介、使用IDEA开发程序、IDEA常用快捷键、IDEA其他操作
18 3