go 语言设计的理解 - 工程化语言
go语言设计的一点个人理解
go是面向工程实践设计的语言,不追求理论上的严谨性,追求程序员书写简单,开发高效。
go 自身的灵活本身也是双刃剑,有导致其面向对象封装思想的丢失的潜在风险,不适合单个大规模项目。
go 在比较性,赋值,等方面内涵更晦涩,容易出错。
1:go语言设计哲学
面向工程实践设计的语言
不最求理论上的严谨性,而根据工作实践提炼出来的,追求程序员书写简单,开发高效。
语法简单,入手难度低。面对工作中经常使用的内容, 内置实现。
可能最初就是谷歌内部为了自己方便设计的工程语言,为方便快速开发设计
1:api语法简单,尽量减少代码量,提高开发效率
零变量(非指针自动初始化),类型推导(:=),多返回值,去除面向对象的继承等,内置协程等,看上去这些特点在编程思想上没有任何突破,也都是一些小技巧,但对程序员来说确是使用最多的地方,这些便利提供切实的编码效率提升。--提高开发效率就节省了最大的研发成本-人力,时间。 人+时间就是最贵的。
创造价值--可以是别人提供不了的独创技术与思想,也可以是对最常用的地方进行简化提高效率。
2:规范化
同一文件内属于一个包,大小写决定可见性
包引用了就必须使用,局部变量定义了就必须使用否则编译不过
测试支持:按规范命名,提供测试命令
按规范组织文件路径,可方便分享使用
工具链:go :vet,godoc ,get,fmt
3:构建
编译时只编译需要的代码文件,不需要的不编译。
go get方便拉取外部引用
直接生成单个可执行文件。all in one,方便微服务。
4:并发
现代的程序并发已经是比不可少的的内容,go语言内置协程,实际是线程被平台托管,类似java 管理对象内存一样。
5:面向对象的调整(封装的灵活性)
程序的抽象:数据结构与算法,即数据抽象与算法抽象。面向对象对两者进行和统一,提供类,将数据抽象与算法抽象统一了。但实际工作场景存在很多值需进行数据抽象或算法抽象的场景,比如很多值对象(贫血),很多处理就需要传递一个算法实现特定流程。java强制一切都是对象,会让程序员定义很多孤零零的类(只实现数据封装与单个函数封装-可能是静态函数)。在有些时候只需要结构的重用,算法重用,固化的使用对象反而不灵活。
go 与java 比较
结构体:结构体本身是值对象,同时保留指针。让程序员决定用值对象还是引用。方便定义值对象承载数据封装。
函数为一等成员:跟其他类型一样,可作为函数参数,结构体的成员,返回值等。
回归:将数据,函数封装的灵活性交给程序员。
6:为合作设计
按规范组织包结果,在代码库上的可直接获取 go get ,会递归下载需要包-类似maven
godoc 书写好代码文档,生成接口文档 ,可放在godoc上
go fmt 格式化代码
2:具体内容
2.1 工具链
以工程化,规范化为目标提供常用的工具,主要包括
规范化:go vet ,go fmt, go doc ,godoc
编译运行相关:go build ,go run ,go clean
2.2 面向对象
继承体系
组合替代继承,语法上匿名成员组合,使用时有继承的语法用法-可以用组合类型的对象简短调用组合内匿名成员的方法与字段-语法上的技巧(语法糖)。本质是组合,不是继承。不能将组合类型的对象赋值给器匿名成员类型的变量。
去掉了传统面向对象的一些特性,比如重载。严格意义上重写也没有了。
接口实现
鸭子类型,不用在定义时显示声明实现接口,根据其具有的方法决定。
实现者可以不引用接口定义。灵活,合作方便。
在提倡小接口的情况下尤其灵活。实现者不用引用很多接口。小接口类似函数(功能封装)一样传递。
2.3 并发
实实在在的简单,简单的同时也失去一些灵活性。
简单:协程、通道 提供的新的工具,配合语法上的简单,可以方便简单实现并发,简单是实在的。
高效:要在特点场景才可能,有些场景反而失去了自己对线程的控制,可认为goruntine 共用底层的线程池。如果需要用多个线程池,分配不同的线程数处理不同任务类型,隔离彼此,可自己构建几个gorutine池。但其实这些goruntine池底层还是共用相同的物理线程池。有时达不到自己控制线程的效果。用go反而不便实现。 因此go并发是否高效要看具体场景,或者说是有条件下的高效。有时不高效,反而让控制失效。
chan :go 提倡协程间通过chan(通道,类似生产消费队列)进行通信,chan本质是个生产消费队列,但语言原生内置,在加上跟for rang ,select default 等的语法配合,用chan 实现协程间通信非常简单优雅。
2.4 函数是一等成员
1:函数类型定义与函数声明
函数类型定义:定义个类型,这个类型底层是个函数。
函数声明:定义一个函数实现,包括函数体
函数成员定义:定义函数变量,将一个函数赋值给这个变量,函数可作为函数参数,返回值,结构体的成员等存在。
2:函数类型定义特色
不同函数类型的转型:
两个函数类型名称,类型名不同,但签名相同,如果需要赋值,使用转型。
给函数类型定义方法:
函数类型定义,与其他类型定义一样,也可以给这个类型添加方法。
2.5 内置标准库
后发的优势,将目前用的比较多的内容作为内置库实现,省去引用第三方库,方便。
json,http ,加解密等。
3:go的缺点
3.1 异常困惑
java 的异常 用习惯后,go 的遍地 if err!=nil 会让人很难忍受。
自动往上层抛:java try catch 解决预料内及不可预料异常,如果不处理会自动向上层抛异常,代码内不用手工主动抛出调用其他函数的异常, 感觉很流畅。
异常的类型:异常也像类一样有派别,可以声明预料内的异常,让调用者处理。调用者知道看声明知道会出现哪些类型的异常。java 在这方便是可以在函数中声明多种类型的可预计异常。
go 分为err ,跟panic ,panic 可认为是不可控异常-预料外的。err是预料内的错误。
panic 可自动往上层抛,err不会能,如果忽略就丢失了,没法在外层统一处理,所以代码中要有很多err!=nil 的判断。如果调用一个方法只有反馈的err,程序员忘记处理,就自动丢了,这是很危险的事。
err的类型:err 没有在函数处声明再细一步的分类,调用者通过函数声明不清楚都有哪些预计内的err,要么统一处理,如果要根据err类型采用不同的策略,只有去了解实现后才知道,然后用接口断言(判断返回的err接口具体是哪种类型)判断具体err的类型。这无疑增加调用者的难度,返回哪些预料内错误应该是函数实现者的责任,然后并声明出来的。想象一下,调用这调用一个方法,判断err类型还要看函数的实现,是不是要奔溃。
3.2 接口实现-鸭子类型
实现者不显示定义实现的接口其实是把的双刃剑,灵活的同时也少了规范化,你调整方法时会不会不小心就不符合接口的约束了?当有多种实现者时,如何找到接口的合适的实现者本身并不直观。实现者本身可能清楚,使用者可能就不清楚了。
go的思想可能更偏向于接口后定义,功能提供者实现功能,使用者按自己需要定义接口。或更符合依赖倒置(接口由使用者定义,而不是提供者定义)。
3.3 灵活的面向对象的潜在危害
go可以实现简单的面向对象,部分面向对象的特性没有,算法与数据的封装不是强制性的,如果从go开始编程,思想上可能就不会形成严谨面向对象的思想。
而面向对象有强制封装的意思,如果因为灵活而忽略应该的封装,可能会思想纠过头。面向对象这么长时间建立起程序员的封装的教育再次丢失。而合理封装对于大规模合作的项目又非常重要。
感觉go 不适合大块头的项目,并不是语言本身不能,而是语言的特点导致程序员在用go实现时没有严格的面向对象的约束,可能导致代码组织上出现问题。后续如何找到或培养有很好的面向对象思维的go程序员?
当然目前大块头的单个项目本身不是主流了。项目本身拆分为多个小项目-微服务模式的不算。
3.4 GO 有些概念更复杂晦涩
3.4.1 可比较性
java 中可赋值的变量间就可以比较。可以用==比较,可以作为map的key.
go 中 切片,函数,map 都是不可比较的,不能用==比较两个切片或函数,包含不可比较成员的结构,数组也不可比较(不可比较的传染性)。 不可比较的不能作为map的key ,如果接口指向不可比较的结构体,接口变量间的比较也会异常。接口可以作为map的可以,接口间比较也可以,但这些都是在编译器间可以,运行期如果接口指向了不可比较对象,都会发生panic 。
而这种被切片与函数不可比较延伸到结构的问题,在编码编译期间是不报错的。运行期间才报错。
不可比较具有传染性,因此结构体,接口是否可比较,你必须深入到所有层级,所有成员都是可比较的才行。如果运行很好的程序,某一天将结构体添加了一个字段,即便不使用这个字段,也会导致运行异常。
3.4.2 赋值与拷贝
数据类型
基本类型:数值型,字符串,字符,布尔型。
复合型:数组,切片,map,结构体
函数与接口
channel
指针。
赋值的场景:
显示赋值 a=b ,函数调用传递实参 ,方法调用接受者,for rang 获取取容器中的值 。
赋值给接口(也是显示赋值),这些场景都会发送赋值操作。
赋值时发送了什么:
java中除基本类型外都是对象,都是引用,不管赋值还是传参等,操作的都是同一个对象。底层数据是一份。
go中,每次赋值都是拷贝一份,类型本身是指针型时,操作的还是相同的底层数据, 其他的都要理解为拷贝。先执行:a=b 在判断 : a==b? 很多时候都不成立(不能比较,比较不通过,通过了要明白实际也并不是一个,而是两个)。
map,channel ,函数,指针赋值后可以认为仍然是同一个,操作时是对相同的底层数据进行操作。
其他都认为是拷贝,如果要对原始对象操作只能取指针再操作。
有时不能取址:map里放入的v,都是不可取地址的,将一个结构体放入map后,只能读取,不能对这个结构体的某个成员进行赋值。将map的v 存放结构体指针,才可以实现修改v的成员。
go的赋值--不直观--第一眼看到的可能跟实际不一样
版权声明: 本文为 InfoQ 作者【superman】的原创文章。
原文链接:【http://xie.infoq.cn/article/94a41b5019927fa99dee3b111】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论