Golang的罪与罚
Go语言特点
Golang(Go)是一门出色的编程语言,以其简洁、高效和易用而受到广泛欢迎。在设计Golang时,语言的创建者们深思熟虑地融入了许多独特的设计思路和哲学,使其在各种场景下呈现出强大的功能和优势。本文将探讨Golang的语言设计以及背后的思考和哲学,并与其他语言进行横向对比。
简洁和高效的设计
Golang的语言设计注重简洁和高效。它丢弃了许多其他语言中繁琐和冗长的语法和特性,将代码精简到了最简形式。这使得Golang代码易于阅读、编写和维护,并且在运行时具有卓越的性能。与其他语言相比,Golang的编译速度非常快,这意味着开发者可以更快地构建和测试他们的应用程序。
并发编程的支持
Golang在设计中专注于支持并发编程,使得编写高效的并发代码变得更加容易。它提供了轻量级的Go协程(goroutine)和通道(channel)机制,可以用于在程序中实现并发和消息传递。通过这种方式,开发者可以更好地利用多核处理器,提高程序的性能。
内存管理
Golang在内存管理方面有独特的设计。它使用了一种称为垃圾回收(garbage collection)的机制,自动管理动态分配的内存。这减轻了开发者的负担,不再需要手动分配和释放内存。垃圾回收器可以自动识别和回收不再使用的内存,从而避免内存泄漏和许多其他内存相关的错误。
开放的标准库
Golang提供了一个丰富的标准库,其中包含许多常用的功能和工具,可以帮助开发者更轻松地构建应用程序。这些包括文件操作、网络编程、加密解密、并发编程等。通过使用这些标准库,开发者可以节省大量的时间和精力,而不必从头编写所有的功能。
goroutine
golang中最重要的特色就是goroutine。从函数式角度出发这就是用户维度的continuation,golang并不会让代码的性能变得更高,Java存在Thread和纤程之后理论上并发的处理能力并不一定比golang差。erlang也很早就能支持这种用户维度的线程。golang做得比较好的一点是做了非常方便的封装
go func(){
}
就可以启动一个新的协程
二进制编译
所有golang的代码都是可以静态编译的,启动的时候也不存在一个JVM的虚拟机需要先启动,所以相对而言对于小工程来说golang能得到一个非常小的可执行文件,内存占用也相对较小。但是这一点对于一些企业级的应用上来说还真的不一定,这时候最终打包出来的镜像和应用占用的内存消耗并不一定比java少多少,大概率还是在一个量级的。
工具链
Golang 官方提供了一系列开发工具链,这使得在vim或者emacs这样的编辑器中也能获得不错的开发体验。
学习编程语言
语言都是用来交流,描述事物的。编程语言也是一种语言,和自然语言不同的是:编程语言是准确的(即使LLM成熟了可能也很难解决自然语言二义性的问题,这是自然语言定义的本质上带来的问题)、运行在计算机(人脑也可以解析)上的一门语言,用来和计算机交流的一门语言。
既然是一门语言,和学习自然语言类似,我们在学习的时候都会经历一些相同的步骤。学习语言的基本组成(词语、语法等等);学习怎么使用这些基础组件 组合出丰富的表达能力(怎么遣词造句、组成段落);学习怎么凝练地描述一个概念。翻译上有所谓的信雅达的概念,在使用自然语言和编程语言的时候其实也存在这个问题——如何将一个事物描述得凝练准确。
提供了哪些基础能力和类型?
Golang 提供了基础类型包括:
- string
- int (int32 int64)
- bool
基本的数据结构
- array 数组,几乎所有的编程语言都提供了这种基础的数据类型,因为这就是计算机内存中一片连续的内存空间,数据的长度是固定的。
- slice 切片,有点类似c++中的vector,是可以动态增长的数据,程序员不需要关心需要开辟多大的内存,这个数据结构可以自动扩容、缩容。
- map
数据操作
- make 用来新建数组或者map,还有chan
如何构建复杂的模型?
golang 提供了struct 来构建复杂的数据结构
// 直角坐标点
type Point struct{
x float64,
y float64
}
// 极坐标点
type RPoint struct{
// 半径
r float64,
// 角度
d floadt64
}
// 线段,两点确定一个直线
type Line struct{
a Point,
b Point,
}
通过struct可以构造出复杂的数据结构来给各种业务进行建模,对比java的方式是通过class来定义数据结构。class可以认为是struct的扩展,但是java本身的设计限制了像c++那样的多重继承,而且java的设计模式中也不推荐过于依赖继承的方式来扩展数据模型,多重继承也是被限制的。(组合优于扩展) 那么要怎么扩展和一个已有的结构呢?
golang提供了一嵌入的机制来实现结构的复用,java的方式是继承。
如何进行抽象?
首先,什么是抽象呢?编程一般来说都是对实体世界的建模,现实世界一直都是复杂和具体的,需要在已有的基础上进行提炼和抽象,来解决和描述问题。 解决问题有时候和描述问题是同一个维度的,当你的模型在描述问题上非常通顺的时候,通常在解决问题上也会很流畅。编程语言的抽象能力让我在更高的维度来描述模型之间的关系或者业务逻辑,而不用关心具体实现。 例如关于一个人的上班的描述: a.起床 a.通勤(目的地:公司) a.工作 a.吃饭(午饭) a.午休 a.工作 a.吃饭(晚饭) a.工作 a.通勤(目的地:回家)
这是一个抽象的描述,关于打工人的一天,不需要知道A的具体工种是码农、产品、业务、司机、保安、帕鲁。可能都有打工人的这几个基本能力。
interface
接口
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Rectangle struct {
Width, Height float64
}
type Circle struct {
Radius float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func getArea(shape Shape) {
fmt.Println(shape.Area())
}
func main() {
r := Rectangle{Width: 7, Height: 8}
c := Circle{Radius: 5}
getArea(r)
getArea(c)
}
🦆duck_type
鸭子类型: 如果他叫起来像鸭子,他飞起来像鸭子,那么他就是一只鸭子。golang的interface没有implement关键字。
在Java中会明确指定 A implement B。A实现了B接口。 golang的选择是更加注重具体的行为,如果支持B的所有方法,那么A就是一个B的类型。
简洁性:不需要显式声明类型实现了哪个接口,这样可以减少冗余代码,使得代码更加简洁。会有部分的代码清晰性的丧失,不知道类型实现了哪些接口,好在IDE和代码工具可以弥补这个缺陷。
解耦:类型和接口之间的实现关系是松散的,类型不需要知道接口的存在,反之亦然。这增加了代码的灵活性和可重用性。
可扩展性:可以在不修改现有代码的情况下,通过添加新的类型来扩展程序的功能,只要这些新类型满足接口的要求。现有的代码不需要修改,这对于已有的代码新增抽象和类型十分方便,不需要修改库或者历史代码。
first class function
可以把函数当作是参数传递
范型
golang 的范型支持相比java来说要弱很多,使用起来没有那么方便
存在的问题
多值返回与错误处理
在golang中经常可以看到下面类型的代码
ret, err := foo(x, y, z)
if err != nil {
return err
}
这是一个约定俗成的使用多值返回来处理错误的方式。设计的初衷是希望程序员能够显式处理掉所有的err,所以把err也作为一个返回值。 期望的结果
- 正常返回是 ret!= nil 且 err ==nil
- 异常返回是 ret ==nil 且 err !=nil 但是这只是个约定,实际上我们能看出来,有可能存在2*2 总共4种可能的返回结果。还有程序员的err返回的bool类型,表示处理成功或失败(没有遵守约定,我就被坑过)。其他的几种类型的返回可能是什么原因呢?
- ret!=nil 且 err !=nil ,也许foo已经处理了一半的结果,但是中途出错了,那么这个一般的结果是否是需要的呢?这就只能具体情况具体分析了
- ret is nil 且 err is nil : 没有错误,也没有返回结果,这个实际开发中比较少见,可能是业务返回的nil是合理的返回结果之一,所以也自然没有错误。
还有一个问题,编译器检查的只是err是不是被使用了,但是这个err的类型是什么都没有check,可能出现的err是一个字符串的包装,其实并不严谨。
其他语言有存在 union type的概念,可以写作 {String, FileNotFound},表示一个函数要么返回String ,要么是文件没找到的错误。这样返回的结果就不会出现歧义了,避免了各种类型的混淆。
Java的异常处理使用了try catch 以及exception,实际开发中为了不让接口奔溃你可能需要在每一个处理入口处加上一个大的try catch 。你不清楚底层的代码可能有哪一块报错,所以为了保险起见 catch所有可能的错误。
defer的顺序
defer的定义用来确保一些资源的释放,锁的unlock工作可以被执行完,用来执行一些收尾工作。 但是当做个defer被定义的时候他们的执行顺序是倒过来的,可以简单理解为每defer一个函数就往stack中加入一个待执行的任务,最后执行的时候是从栈顶开始弹出每个要执行的任务,所以最先声明的defer会最后执行,这在一定程度上给不熟悉人带来了一些潜在隐患。
Interface的问题
可能的问题: 可能你的类型无意之间实现了某一个接口,但是由于没有显式申明,如果你某天突然修改了类型的函数定义的话,可能突然会有编译报错(概率较小),因为类型发生了变化。
nil接收器
golang中方法调用和Java中不一样。下面的方法调用也是合法的。
nil.someMethod()
所以在使用的时候如果接收器是指针类型,开发者需要在代码内部判断接收器是否为空
但是也有个好处,下面的方法级联调用也是可以实现的,不会出现空指针错误。
nil.GetA().GetB().GetC()
总结
Golang的语言设计以及背后的思考和哲学使其成为一门优秀的编程语言。其简洁、高效的设计、对并发编程的内置支持、内存管理机制的优雅设计以及丰富的标准库,使得Golang在开发各种类型的应用程序时具有优势。与其他语言相比,Golang在相同的场景下展现出了独特的设计思路和优势。无论是对新手还是经验丰富的开发者来说,Golang都是一门值得学习和应用的语言。
怎么学习Go语言呢?
多实践:
- 尝试将golang应用在实际开发中
- 每一门语言都有其对应的擅长应用场景