互联网技术日新月异,当时的服务器软件系统规模庞大,程序都是由众多程序员共同创作,源代码也以数百万行计,而且实际上还需要每天都进行更新。
我们来从官方go起源来寻找问题:
AtthetimeofGo'sinception,onlyadecadeago,theprogrammingworldwasdifferentfromtoday.ProductionsoftwarewasusuallywritteninC++orJava,GitHubdidnotexist,mostcomputerswerenotyetmultiprocessors,andotherthanVisualStudioandEclipsetherewerefewIDEsorotherhigh-leveltoolsavailableatall,letaloneforfreeontheInternet.
译文:在Go诞生之时,也就是十年前,编程世界与今天不同。生产软件通常是用C++或Java编写的,GitHub不存在,大多数计算机还不是多处理器,除了VisualStudio和Eclipse之外,很少有IDE或其他高级工具可用,更不用说在互联网上免费了。
Meanwhile,wehadbecomefrustratedbytheunduecomplexityrequiredtousethelanguagesweworkedwithtodevelopserversoftware.ComputershadbecomeenormouslyquickersincelanguagessuchasC,C++andJavawerefirstdevelopedbuttheactofprogramminghadnotitselfadvancednearlyasmuch.Also,itwasclearthatmultiprocessorswerebecominguniversalbutmostlanguagesofferedlittlehelptoprogramthemefficientlyandsafely.
译文:与此同时,我们对使用我们所使用的语言来开发服务器软件所需的过度复杂性感到沮丧。自从C、C++和Java等语言首次开发以来,计算机的速度已经变得非常快,但编程行为本身却没有进步那么多。此外,很明显,多处理器正在变得普遍,但大多数语言对高效、安全地对其进行编程几乎没有提供帮助。
说是讨论,倒不如说是吐槽更好,他们一致认为:与其在臃肿的语言上不断增加新的特性,不如简化编程语言,其他的编程语言老前辈,都是有严重的历史包袱,于是Go语言就是在这样的环境下出现的,Go语言为了解决高难度、低效率、高耗资源的系统编程问题。
Wedecidedtotakeastepbackandthinkaboutwhatmajorissuesweregoingtodominatesoftwareengineeringintheyearsaheadastechnologydeveloped,andhowanewlanguagemighthelpaddressthem.Forinstance,theriseofmulticoreCPUsarguedthatalanguageshouldprovidefirst-classsupportforsomesortofconcurrencyorparallelism.Andtomakeresourcemanagementtractableinalargeconcurrentprogram,garbagecollection,oratleastsomesortofsafeautomaticmemorymanagementwasrequired.
译文:我们决定退后一步,思考随着技术的发展,未来几年哪些主要问题将主导软件工程,以及新语言如何帮助解决这些问题。例如,多核CPU的兴起表明,语言应该为某种并发或并行性提供一流的支持。为了使大型并发程序中的资源管理易于处理,需要垃圾收集,或者至少需要某种安全的自动内存管理。
最初的讨论是在2007年9月20日星期四下午进行的。第二天下午2点,RobertGriesemer、RobPike和KenThompson在Google山上43号楼的雅温得会议室举行了有组织的会议查看校园。该语言的名称于25日出现,第一个邮件线程中出现了几条关于该设计的消息:
由罗布在2007年9月25号回复给肯.罗伯特的有关新的编程语言讨论主题的邮件。
Subject:Re:proglangdiscussion
From:Rob'Commander'Pike
Date:Tue,Sep25,2007at3:12PM
To:RobertGriesemer,KenThompson
ihadacoupleofthoughtsonthedrivehome.
'go'.youcaninventreasonsforthisnamebutithasniceproperties.
it'sshort,easytotype.tools:goc,gol,goa.ifthere'saninteractive
debugger/interpreteritcouldjustbecalled'go'.thesuffixis.go
邮件正文大意为:在开车回家的路上我得到了些灵感,给这门编程语言取名为“go”,它很简短,易书写。工具类可以命名为:goc、gol、goa。交互式的调试工具也可以直接命名为“go”,语言文件后缀名为.go等等。
该语言是Go还是Golang?
该语言称为Go。“golang”这个绰号的出现是因为该网站最初是golang.org。(当时还没有.dev域。)不过,许多人使用golang名称,并且它作为标签很方便。例如,该语言的Twitter标签是“#golang”。无论如何,该语言的名称就是简单的Go。
GoatGoogle:LanguageDesignintheServiceofSoftwareEngineering
译:为软件工程服务的语言设计
这是由罗布·派克阐述的,在当时Go的设计就是为了Google公司的软件工程问题。
workingwithGoisintendedtobefast:itshouldtakeatmostafewsecondstobuildalargeexecutableonasinglecomputer.
译:使用Go的目的是快速:它应该最多只需几秒钟即可在单台计算机上生成大型可执行文件。
软件工程指导了Go的设计:构建一种轻量级且令人愉悦的高效编译编程语言。
Go语言从开源到现在,已经有多年了,虽然Go语言作为一个标准的富二代,一开始就赢在了人生的起跑线上,有Google富爸爸撑腰,但是发展过程还是比较坎坷。
2016年发生了什么?为什么又重新热起来了呢?
我们来看看Go语言的历代版本
2013年5月—Go1.1:这个版本的Go致力于增强语言特性(编译器、垃圾回收机制、映射、goroutine调度器)与性能。
2013年13月—Go1.2:"Three-indexslices"Go1.2之前,切片的参数只允许有两个,在Go1.2中因为引入了新的语法,即支持第三个参数,可以调整切片的容量....
2014年6月—Go1.3:堆栈管理在此版本中得到了重要改善。
2014年12月—Go1.4:该版本主要侧重于实现工作、改进垃圾收集器并为在接下来的几个版本中推出的完全并发收集器奠定基础。
2016年2月—Go1.6:增加对于HTTP/2协议的默认支持,再一次降低了垃圾回收器的延迟runtime改变了打印程序结束恐慌的方式。现在只打印发生panic的goroutine的堆栈,而不是所有现有的goroutine
2016年8月—Go1.7:Context包转正,垃圾收集器加速和标准库优化
.....
"垃圾回收器被完全重新设计实现",也叫GC。这个是重点,Go在吃了这么多年苦后,也虚心学习古老的GC技术,让它能重新回暖。
在随后的几个版本中,Go都把GC的优化放在改进的重心点。
语法是构成编程语言的基石,Go语言的语法与其他语言相比来看是比较简洁的,但也说明了Go语言的可读性或许不如其他语言强。以下列出了几点Go语法的特性:
在C/C++、Java的语法中,我们通常需要自己输入分号来结束一个语句,但Go语言的代码编写中,无需我们手动输入分号。Go中的词法分析器会根据规则来自动检测是否需要插入分号(然而分号不会在你的代码中出现),通常情况下,在你按下回车后就会自动在行尾插入分号。
const(a=iota//a=iota=0b//b=1c//c=2)2.1.4函数的返回值Go中的函数可以返回多个值,并且多个返回值的类型可以是不同的。在有需要的情况下我们给返回值命名,返回值得有意义才可以。除此之外,Go语言的多返回值也可以用来进行异常处理,函数调用出现错误可以返回一个error类型的值来指示错误。下面这段就展示了Go中多返回值的特性和相比单返回值的优势。
funcWrite(b[]byte)(nint,errerror){...}
Write就是该方法的名称,那么后面的这个括号的n和err就是两个返回值,分别是int和error类型。
在Go语言中其实并没有我们传统意义上类似trycatch这种错误处理的工具和包。在Go中错误通过一个内置的结构类型来表示,程序员在使用这个接口的时候不仅可以用来判断错误,还可以自定义错误类型,提供一些上下文等。
Go语言采用了把错误看作一个值的方式来进行处理,再加上Go语言多返回值的特性,这样使得函数在返回正常值的同时也能返回错误。Go确实也提供两个可以来处理异常的内置函数(panic和recover),但这两个内置函数大多用来处理不可恢复的错误,并且各自都有一些特点并不像error那么通用。所以Go语言错误处理方式是相对繁琐一些的,但这种直截了当的显示设计让代码更加清晰易读抵消了它的冗长性。
typeerrorinterface{Error()string}2.1.6语法糖语法糖(Syntacticsugar)这个名字挺有意思的,是一个应该的计算机科学家发明的词。指在编程语言中添加的一些语法,这些语法对语言本身的功能没有什么影响,而且还能为编程者提供便利。
在一行代码中对进行多个变量的赋值操作,例子如下:x,y=y,x//交换x和y的值
当我们在定义一个函数,有两个返回值,但实际上只有一个用得上,那我们还可以用_将不需要的值忽略。例子如下:
funcdivide(a,bfloat64)(float64,error){ifb==0.0{return0.0,errors.New("divisionbyzero")}returna/b,nil}funcmain(){result,_:=divide(10.0,2.0)//只关心结果,不关心错误fmt.Println(result)}除此之外还有一些其他语言也会见到的一些语法糖就不一一列举了,比如:自增(++)自减(--)、breakcontinue等。总之这些语法糖可以让人们在使用Go语言编程时使代码更简洁清晰,减少代码冗余,让语言更具功能性更优雅。
在Go语言中官方给出的数据结构有:数组(Array)、切片(Slice)、映射(Map)。数组和其他的编程语言类似,用于存储同一类型和固定长度的连续数据结构。切片像是更灵活的数据结构,可以看做是可动态的扩容和缩减数组。映射也与其他编程语言类似,是用于存储键值对的一个哈希表。
相比之下,Go语言没有像链表、队列、栈、树等一些常用的数据结构,但Go语言的这三个基本数据结构足够强,可以用他们来实现这些数据结构。这也和Go语言的保持语言简单避免语言过度臃肿的设计理念相符合。
在几年前的Go是没有泛型这个概念的,直到2022年Go语言的核心团队在Go1.18版本中引入了泛型的概念。初次看见Go泛型,文档给出的很多概念:Typeparameter(类型形参)、Typeargument(类型实参)、Typeconstraint(类型约束)、泛型类型(Generictype)......初次看见啊这些概念时候就算是翻译成中文大概率也不明白他们分别用处和意义是什么。
我们引入一个非常经典的泛型函数例子来介绍类型形参、类型实参和类型约束:
funcPrint[Tany](s[]T){for_,v:=ranges{fmt.Println(v)}}ints:=[]int{1,2,3}Print(ints)//在这里,int是类型实参我们再引入一段代码来解释什么是泛型类型:
packagemainimport"fmt"//定义一个泛型切片类型typeGenericSlice[Tany][]T//添加元素到泛型切片func(s*GenericSlice[T])Append(valueT){*s=append(*s,value)}//获取泛型切片中的指定元素func(sGenericSlice[T])Get(indexint)T{returns[index]}funcmain(){//使用int类型实例化GenericSliceintSlice:=GenericSlice[int]{}intSlice.Append(1)intSlice.Append(2)fmt.Println(intSlice.Get(0))//输出:1fmt.Println(intSlice.Get(1))//输出:2//使用string类型实例化GenericSlicestringSlice:=GenericSlice[string]{}stringSlice.Append("Hello")stringSlice.Append("World")fmt.Println(stringSlice.Get(0))//输出:Hellofmt.Println(stringSlice.Get(1))//输出:World}typeGenericSlice[Tany][]T这段代码就是定义了一个泛型切片类型,那么GenericSlice个泛型类型,该类型可以存储任何类型的元素。如果我们想强制该类型只能存储int、float64和string的数据可以使用这样的方式来定义泛型类型:typeGenericSlice[Tint|float64|string][]T,这里面的int|float64|string也叫做类型约束。
后面的代码就是一些对于不同的类型在泛型切片类型中的具体实例化和使用。除此之外与其他语言相比,在Go中,我们可以使用泛型隐式调用接口并且泛型支持类型判断。假设我们现在有一个Adder的接口,定义一个Add方法,实现了Add方法的类型可以被认为是Adder类型。
packagemainimport"fmt"//Adder接口定义了Add方法typeAdderinterface{Add(a,bint)int}//IntAdder实现了Adder接口typeIntAdderstruct{}func(IntAdder)Add(a,bint)int{returna+b}//使用泛型函数调用Add方法funcsum[TAdder](a,bint,adderT)int{returnadder.Add(a,b)}funcmain(){adder:=IntAdder{}result:=sum(1,2,adder)//类型推断,不需要显式指定IntAdder为Tfmt.Println(result)//输出:3}IntAdder隐式实现了Adder及接口,因为他定义了接口要求的Add方法,这就是隐式接口实现。下面的代码,我们要调用sum函数时候,我们不需要显示地指出IntAdder是T的类型,编译器可以自动判断出来。
这些就是Go泛型中一些比较基本的用法和概念,更多的关于泛型的内容不在这里赘述了。
Go语言拥有强大的编译器工具链,其中包括gobuild、gofmt等,这些工具在给不用平台的用户提供了一致的体验和操作。Go语言的标准库包中含有大量的操作系统抽象,如文件操作系统、网络通信等。Go语言能在Winddows、Linux、MacOS三个不同的操作系统下进行交叉编译都离不开上面这些工具。
一个简单的例子,如果我们在MacOS环境上开发的,而且希望编译一个能在Windows上运行的程序,我们可以在gobuild之前加上这么一串代码即可:GOOS=windowsGOARCH=amd64gobuild。这些特性使得Go语言在开发分布式系统和微服务时尤为受欢迎,因为它们允许开发者在一个平台上开发和测试,然后轻松部署到其他平台上运行。
Go语言的工具有很多,可以分为一下几类:
网络上也有很多Golang的优秀辅助工具,比如Lancet(柳叶刀)
lancet受到了javaapachecommon包和lodash.js的启发,lancet有很多有帮助的特性。
Golang的生态系统怎么样?都在使用Go做什么,一般搭配着什么来使用呢?这里收集了一些统计图。
可以发现Go的新版本非常多人用,大部分人都愿意使用Go的新版本。而与此相反的Java语言就有明显区别。
要知道Java8可是2014年3月18日发布的,这个版本是一个伟大的里程碑,它为Java带来了更现代,更强大的功能。同时这也是不幸的事情,就连MaritvanDijk(JetBrains的技术布道师和JavaChampion)都说:
“It’sunfortunatetoseesomanypeoplestillusingJava8(andolder).Iwonderwhat’skeepingthemfromupgradingtonewerversionsandgettingaccesstogreatnewlanguagefeatures,andhowwecanhelpthemmigratetheircodetonewerJavaversions.”
译文:很遗憾看到这么多人仍在使用Java8(和更早版本)。我想知道是什么阻止了他们升级到新版本,让他们无法使用出色的新语言功能,以及我们如何帮助他们将代码迁移到新的Java版本。
从Go版本使用情况和Java版本使用情况对比发现,Java是"恋久"的、Go是"追新"的,这也体现出了Go具有出色的向后兼容性。
Golang的优质中文社区比较稀少。
稀土掘金也是一个可以讨论学习Golang的网站,还有奖励可以拿,很推荐。尽管社区数量有限,但这些资源仍然可以帮助我们深入学习和探索Golang。
谈到Golang的并发和性能,常常会听到两种声音:
甚至有很多争论的声音。我认为,我们对这两个说法都不要相信,而是应该考虑到,从各自的立场出发的话,他们都是有理由的。我们自己睁眼看,动手做,亲自去体会即可。那谈到Go的性能,我们从何谈起呢?从Goroutine和Channel谈起吧!
Go的设计初衷之一就是要简化并发编程,于是它引入了协程(Goroutine)和管道(Channel)这两个特性来控制并发。它们都是什么呢?怎么就简化了并发编程了呢?我们先来看看他们分别都是什么。
p.s:我这里所提到的协程和管道,特指Go语言中的Goroutine和Channel。
谈到协程,我先简单介绍进程和线程。如果都用一句话解释的话,可理解为:
一个进程可以有多个线程,它们可以共享所属进程的内存,不需要内存管理单元处理上下文的切换,既然有了相比于进程更为轻量的线程,那么Go为什么还出现了协程呢?大体是因为,每一个线程所占用的内存空间还是比较大的,还有就是CPU在多个线程间来回调度的时候,额外的开销还是比较大的。这些在Go语言看起来,都不能忍受。
所以它的调度器使用与计算机CPU数量相等的线程来减少线程间的频繁调度,就省去了线程间来回调度的额外开销。同时又在线程上面开了多个Goroutine来减少执行的开销,提升效率。
就好比,以前一个工厂(进程)只有一条流水线(线程),老板想要提升效率,增加了一个工厂中的流水线数量。但是需要工人(CPU)来回在几个流水线上加工对应的产品(执行程序)。而现在呢?一个工厂中有几个工人就有几条流水线,每条流水线上有多个产品需要加工,一个工人只负责一条流水线,就省去了工人跑来跑去的麻烦。
简单了解了什么是Goroutine,那么什么是Channel呢?由于Go语言推崇使用通信的方式来共享内存,底层抽象出了一个更高级的数据结构Channel用于协程间的通信,使模块间解耦并且致力于让并发变得更简单。
比如下面是利用Channel实现的,在两Goroutine中交替的输出奇数和偶数:
可以看到,我们利用Channel,安全的在两个Goroutine之间传递了信号,使得并发在较为安全的前提下,变得如此简单。基本了解了Goroutine和Channel后,我们再来看看,Go现在的调度模型,是如何高效调度的。
如上图所示,Goruntime的调度器维护了一个全局的协程队列,并且给每一个线程M上绑定了一个处理器P。每个P的本地又维护了一组协程队列。那么什么是HandOff和StealWork呢?
我们可以将StealWork看做是,某个处理器中没有协程需要处理了,与之绑定的线程空闲了,并且全局也没有需要执行的协程了,那么去帮帮兄弟线程。从兄弟那里去“偷”几个协程过来帮他执行。所以通过工作窃取方式,可以对任务进行再分配实现任务的平衡。
如下图所示:
当简单了解了Go语言runtime的调度模型,来看看它大致是如何进行内存管理。
要来管理内存,首先得了解Go的内存模型是怎样的。
首先了解一点点前置知识:Go语言为了防止内存碎片化,采用了分级隔离策略来提高内存的利用率。将其最小内存管理单元分为了68个级别的mspan。了解了这个点,来看看上图。为了提高访问效率、减少锁竞争等问题,Go语言将其内存分成了由HeapArena、MCentral、MCache构成的三级缓存。
我们从下至上,依次来看看是哪几层:我们将物理内存转化为虚拟内存后,Go语言将这大小为256TB的庞大虚拟内存转变成了222个大小为64MB的HeapArena。为什么刚好这么多个呢?因为虚拟内存最大为256TB,而222*64MB,刚好等于这么大的内存。这所有的HeapArena共同构成了Go语言的堆内存MHeap。
然后程序想要分配内存,需要获取MSpan,因为这是最小的内存管理单元。想要获取MSpan,我们得从HeapArena中获取,但由于有68个等级的MSpan,想要获取对应等级的MSpan,得挨个遍历才能获取。为了快速得到想要的MSpan,所以创建了一个中心索引缓存MCentral。将每个等级的MSpan,分了需要GC和不需要GC(scan和noscan)扫描的两条链表,共134条,为什么是134条呢?因为其中有一个等级的mspan很特殊——0级的MSpan——它是直接放入HeapArena中的,所以67*2,只有134条。
那么想要获取MSpan,就优先会从MCentral中获取了。但是MCentral是全局的资源,是多个协程共享的,需要加锁才能安全的访问。为了防止过多的锁竞争,导致锁饥饿的问题,在每个M上添加了一个本地缓存MCache缓存,其中包含每一级别的MSpan各两个,也是分scan和noscan两种,那么就是68*2,总共136组。有了MCache,这样就不需要每次都从全局中心索引中去获取MSpan了。
当你大致了解了Go语言的内存模型,来看看它是如何进行内存分配的。
Go把所有需要申请内存的家伙都称为对象Object,对于直接分配到堆上的对象,Go会根据对象的大小进行分配。它将其分为三个等级的对象:
进行分配时,根据对象的大小,有不同的操作。如果大小为0,会直接分配一个固定的地址zerebase给它。
其次会检查大小是否在32KB以内。如果是小于16B的tiny对象,并且不需要被垃圾回收器扫描,它会尝试通过MCache中的tiny字段来分配地址。
如果tiny字段已满,它会将tiny放入MCache一个2级的MSpan中,并为tiny分配新的内存地址。如果2级MSpan也满了,将从MCentral中交换一个。
另一种情况是会根据对象的大小,从MCache中获取一个能容纳它的最小级别的MSpan来分配地址。如果对应级别的MSpan满了,则会加锁从MCentral中交换一个对应级别的MSpan。如果连Mcentral对应等级的MSpan链表也满了,将从HeapArena中换一组对应大小的MSpan。如果这一个HeapArena也满了,将向操作系统申请一块新的虚拟内存单元HeapArena。
最后对于大于32KB的对象,首先会计算对象的大小,然后申请对应大小的0级MSpan后,直接放入HeapArena中。
从上面的分配过程,也能看出Go是对内存进行分级和分层管理的,这样不仅有助于提高内存的利用率,对内存的分配效率也有显著的提高。那么分配完内存,如何清理不需要的内存呢?
这就得来谈谈Go的垃圾回收了(GarbageCollection,也称为GC),Go目前采用的是“三色标记法的标记清除+混合屏障”的垃圾回收策略。那么什么是三色标记法呢?什么又是混合屏障呢?
三色标记清除法源于标记清除,思路非常简单:扫描内存标记出需要清理的对象,然后在回收掉即可。其中核心是标记,只要我们正确标记了需要清理的无用对象,清扫其实很简单。
其过程简单来说就是:一开始将所有对象都置为白色,表示暂时无用的对象。然后将所有的GCRoot对象放入灰色集合,利用可达性分析扫描GCRoot能够到达的对象,将能到达的对象放入灰色集合的同时将扫描完成的对象放入黑色集合。
当灰色集合中的所有对象都被扫描分析完成时,意味着标记结束了,程序中只有白色和黑色两种颜色的对象,然后清理掉还是白色标记的对象即可。
其实这个过程,即使使用普通的“标记”方式,也能达到类似的效果,那么为什么还要费尽心思搞成三色标记法呢?其实是为了更好的增加混合屏障,让垃圾清理的效率和标记的安全性做一个平衡。
这里提到的混合屏障,包含插入写屏障和删除屏障两种。为什么要引入两种屏障呢?这个核心点还是在于Go想要提升垃圾回收的效率。
我们可以试想一下,如果垃圾回收的过程和用户程序是串行化执行的。当需要进行垃圾回收的时候,直接暂停所有正在执行的程序,标记清理完成后再继续执行。这个过程叫做STW(StopTheWorld),如它的名字表明的一样,暂停这个“世界”,我要开始进行垃圾回收了!
这其实就是Go最早期的垃圾回收策略。这样串行化的好处很明显:实现简单、安全...唯一的缺点恐怕就是不够快。但这唯一的缺点也是它致命的缺点,Go不断迭代的垃圾回收器,都在致力于平衡效率和安全的关系。
在三色标记的过程中,用户的程序正和垃圾回收在并发的执行,突然有个还未扫描的灰色对象的下游对象指向了一个已分析扫描完成的黑色对象。这样在此轮扫描结束后,那个乱跑的对象,就会被当做垃圾清理掉,但其实它是一个有用对象。
所以为了限制这样乱跑的对象,就增加了一种“插入”写屏障的机制。即当有对象莫名其妙插入黑色对象上时,直接将其设置为灰色,代表一会还要检查它。那么在此轮扫描结束后,最终也能保证不会误清理乱跑掉的对象。
但是Go对栈要求要有非常快的响应速度,以供函数调用频繁的入栈和出栈,在栈上使用屏障会影响性能,所以这种屏障只能在堆上使用。如果栈上不能使用屏障机制,那么有堆对象要跑到栈上,或者有栈对象跑到堆上,就可能会出现误清理的情况。
所以引入了删除屏障,配合写屏障一起使用,在堆上开启了混合屏障。
至于为什么呢?稍后解释,先来看看什么是删除屏障。当有一个待扫描对象突然删除了它某一下游对象时,防止跑掉的下游对象被添加到某已扫描的黑色对象上,我们将被删除的下游对象设置为灰色,放入灰色队列中等待分析扫描。
拥有了混合屏障,对于栈空间和堆空间上的对象,有这些可能:
所以当你了解了有这几种情况的时候,我们可以简单的理解为:
其实这些信息都对,我不反驳,但一个人和一件事能存在于世,定有他的作用与道理。语言技术也是一样的,Golang可能如大家所言。
在企业里,语言并没有那么重要,重要的是问题的解决方案,团队招人的时候,招的基本都是后端Title,并没有限定语言为Golang,如果语言那么重要,干嘛不精确一点。况且据我耳闻,经常听到大家谈论某一技术细节,基本没有拿着xxx语法、xxx框架的使用来讨论的。
没有烂代码,只有垃圾代码,只有屎山代码,不知道我说这话你认不认同。在公司里,我见过很优秀的Golang代码,同样也见过很烂的Java代码。这你能说,代码写的烂,语法很特殊,就能让这个人的开发习惯、编码风格从好变成坏?从乐色变成牛逼?
说平时写代码根本用不到它并发的,我只能说你对它还不够熟悉。开箱即用的Goroutine、Channel、各种同步工具,你都用不明白,还能把什么更高级的并发工具用好呢?我自己写过的两个较大的需求:批量推送、智能创建,列这个名字的目的,就是想说这就是很基础的业务逻辑,但无一没有用到它提供的并发工具,并且最终的性能,都是真金白银跑出来的。
关于其他的,其实我也不想多说,其实它好与不好,一点都不重要。每个人都有缺点和优点,人生如此,技术亦然。如果你非要跟我争论谁好谁不好,没关系,一切以你为准。