myfreax

Javascript开发者如何快速学习Go语言

通常,开发者在某个时间段都会同时使用多个编程语言。经常在语言之间的上下文也有可能导致发生错误的结果。在本文中我们将说明Golang与javascript的区别,它可以让你快速上手或者学习Golang语言

13 min read
By myfreax
Javascript开发者如何快速学习Go语言

通常,开发者在某个时间段都会同时使用多个编程语言。经常在语言之间的上下文也有可能导致发生错误的结果。例如,如果你在Python和Javascript之间来回切换,很有可能你会错误地评估一个空数组array是true还是false。同样的,如果你在Go与Javascript之间来回切换,有可能你会错误地评估switch声名的默认行为是往下走还是跳过。总结语言之间的差异可以帮助你解决这些潜在问题,并且可以更容易地在多个语言来回切换

这个文档是 Golang (or "Go") and ECMAScript (or "Javascript" / "JS")两个编程语言之间的比较。这两个都是很流行的编程语言。但它们并不相似。Javascript是事件驱动,动态类型的解释性语言,Go是静态类型编译性语言。

在本文中我们将说明Golang与javascript的区别,它可以让你快速上手或者学习Golang语言。

我应该使用那个语言?

你应该始终为正确的任务选择合适的工具。 不幸的是,永远不会有一个简单的公式来指导您选择那种编程语言来完成指定的任务。

内部机制

(S) Heap/Stack 内存分配

“堆”和“堆栈”这样的概念在两种语言中都被抽象出来。你不需要担心它。尽管GO有指针,但它在编译时使用escape analysis来计算内存分配。

(S) Garbage Collection(内存回收)

两个语言都实现了GC

(D) Compilation(编译)

Go是需要编译,而Javascript不是,尽管一些Javascript运行时使用JIT编译。从开发人员的经验角度来看,编译语言的最大影响是编译时安全。您可以通过Go获得编译时安全性,而在Javascript中,您可以使用外部代码库来缓解此功能的缺失

并发 & 并行

(D) 概述

描述Javascript并行机制最棒的文章 quote by Felix Geisendörfer:

在Node.js除了你的代码,所有都是并行运行的

不同的Javascript运行时为并发或并行提供了一些选项:NodeJS提供clustering浏览器提供 web workers.

另一方面,Go的并发是通过Goroutines实现,它可以使函数同时运行,使用channels在它们之间进行通信。 虽然Go标准库具有同步原语的“同步”包。更多关于使用Goroutines和channels总结如下:

不要通过共享内存来通讯,应该通过通讯共享内存

(D) 异步 vs 同步 APIs

JS

JS鼓励编写异步API,因为同步API总是会被阻塞,例如:

const fs = require('fs');

// 当文件开始读取时,调用者调用这个函数将会被阻塞
function fetchA() {
   return fs.readFileSync('a.txt');
}

在上面的示例中,异步fs.readFile()是最佳的选择,让fetchA()不会阻塞调用者

Go

在另一方面,Go优先是使用同步API,(see “Avoid concurrency in your API” in https://talks.golang.org/2013/bestpractices.slide#26)
是否被阻塞完全取决于调用者。 考虑下面的类型定义和同步fetchA函数:

type fetchResult struct {
	Message string
	Error   error
}

func fetchA() fetchResult {
	time.Sleep(time.Second * 4)
	return fetchResult{"A data", nil}
}

如果调用者想使用被阻塞方式,那么它可以如下调用这个函数

	a := fetchA()

If the caller does not want to be blocked, then he could call the function inside a goroutine:
如果调用者不想被阻塞,那么它可以使用goroutine调用这个函数

	aChan := make(chan fetchResult, 0)
	go func(c chan fetchResult) {
		c <- fetchA()
	}(aChan)

(D) 串行与并发模式

JS

即使没有并行,我们也可以在串行和并发流程中构造Javascript代码。对于下面的例子,假设fetchA()fetchB()fetchC()都是返回promise的异步函数。

串行

function fetchSequential() {
    fetchA().then(a => {
        console.log(a);
        return fetchB();
    }).then(b => {
        console.log(b);
        return fetchC();
    }).then(c => {
        console.log(c);
    })
}

或者

async function fetchSequential() {
    const a = await fetchA();
    console.log(a);
    const b = await fetchB();
    console.log(b);
    const c = await fetchC();
    console.log(c);
}

并发

function fetchConcurrent() {
    Promise.all([fetchA(), fetchB(), fetchC()]).then(values => {
        console.log(values);
    }
}

或者

async function fetchConcurrent() {
    const values = await Promise.all([fetchA(), fetchB(), fetchC()])
    console.log(values);
}

Go

对于下面的例子,假设fetchB()fetchC()被定义为一个同步函数,类似于前一节中的fetchA(完整的例子可以在这里找到https://play.golang.org/p/2BVwtos4-j)

串行

func fetchSequential() {
	a := fetchA()
	fmt.Println(a)
	b := fetchB()
	fmt.Println(b)
	c := fetchC()
	fmt.Println(c)
}

并发

func fetchConcurrent() {
	aChan := make(chan fetchResult, 0)
	go func(c chan fetchResult) {
		c <- fetchA()
	}(aChan)
	bChan := make(chan fetchResult, 0)
	go func(c chan fetchResult) {
		c <- fetchB()
	}(bChan)
	cChan := make(chan fetchResult, 0)
	go func(c chan fetchResult) {
		c <- fetchC()
	}(cChan)

	// order doesn't really matter!
	// 下面的顺序并不重要
	a := <-aChan
	b := <-bChan
	c := <-cChan
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
}

or


func fetchConcurrent() {
	aChan := make(chan fetchResult, 0)
	bChan := make(chan fetchResult, 0)
	cChan := make(chan fetchResult, 0)

	go func(c chan fetchResult) {
		c <- fetchA()
	}(aChan)
	go func(c chan fetchResult) {
		c <- fetchB()
	}(bChan)
	go func(c chan fetchResult) {
		c <- fetchC()
	}(cChan)

	for i := 0; i < 3; i++ {
		select {
		case a := <-aChan:
			fmt.Println(a)
		case b := <-bChan:
			fmt.Println(b)
		case c := <-cChan:
			fmt.Println(c)

		}
	}
}

模块/包

规范 & 实践

JS

从es6开始,Javascript规范包含一个模块系统,然而AMD和CommonJS已经是流行模块化规范,因为语言开始解决这个问题的时间比较晚。

在es6模块之前,该规范仅支持script模式,其中每个文件共享相同的顶级全局作用域。 这意味着脚本没有“文件作用域”。 在实践中,文件模块作用域很常见,因为它是由代码(window.moduleA = ...),外部工具(requireJS)引入的,或者是由模块系统(NodeJS)中的运行时引入。

Go

Go的导入声明和软件包支持从一开始就是规范的一部分。在Go中没有文件作用域,仅有包的作用域。从Go 1.6(或1.5 +标识)开始使用vendor文件夹更好地支持在项目中封装依赖包。 但是它并不试图解决所有问题

不同点
另一个不同之处在于您的项目中其他内部组件的调用。 在Javascript中,通常一个文件就是一个模块,它们之间调用就必须将文件导入到当前文件中。 另一方面,在Go中,同一个包中的所有文件都可以访问对方,因为Go是没有文件的作用域。

管理

对于Javascript开发,NPM是NodeJS的包管理器,也可以用于客户端项目。 Bower对于客户端项目也很受欢迎。

go get工具只会让你获得最新依赖的主代码。 如果您需要使用固定版本或者进行精确的依赖关系管理,遗憾的是它做不了。 Go社区提出了几个包管理器,这里是一个部分列表:

Go官方启动了自己的依赖管理工具:[dep](https://github.com/golang/dep)。 截至撰写本文时,它仍处于Alpha阶段,而不是官方Go工具链的一部分。 观看该项目[路线图](https://github.com/golang/dep/wiki/Roadmap)了解状态更新!

错误处理

(B) 流控制与值

两个语言都是将错误作为正常的值传递。流控制结构分别是Javascript throw catch finally,Go panic recover defer

(D) 用法

尽管上面提到的相似性,语言在处理错误的方式和时间上有所不同:

JS

在JS中,错误传播的方式取决于函数的性质,如果一个函数是同步的,那么当发生错误时它应该使用throw,并且调用者应该使用try / catch块。

否则,异步函数应该将第一个值传递给回调函数来传播错误,否则它应该返回一个被拒绝的promise。

请注意,async / await机制将通过在try/catch块内处理异步错误。

Go

在Go中,传播错误的方式取决于整个应用程序的严重程度。例如,对于Web服务器应用程序,如果请求处理代码路径中发生错误,则它们不应该使整个服务器崩溃。 因此,这些错误应作为调用者的最后一个参数返回,另一方面,如果在应用程序初始化期间发生错误,则可以认为没有理由继续,因此panic会非常有意义。

(S)堆栈跟踪的丢失

在将错误作为值传递时,一个缺点是丢失跟踪的堆栈。 两种语言都受此影响。 一些运行时间和库试图提供帮助:

JS:longjohn

Go:errgo

关键词 & 语法比较

(D) this 关键词

JS

Inside an object method, this refers to the object (with some exceptions).
在对象方法中,this引用的是一个对象(当然有一些例外)。

Go

在Go中,最接近的类比是方法函数内的接收器,你可以使用this作为接收者:

type Boo struct {
	foo string
}

func (this *Boo) Foo() string {
	return this.foo
}

(D) new 关键词

JS

new Foo() 实例化一个对象从 Foo,它可以是构造函数或者类

Go

new(T)实例化一个类型为T的对象,它会分配一个零值的存储并返回指针, 这与Javascript和大多数其他语言不同,其中new初始化对象,而在Golang中它只有zeros

值得一提的是,idiomatic用一个New前缀命名方法来表示它返回一个指向方法名称后面的类型的指针。 例如:

timer := time.NewTimer(d) //定时器是* time.Timer

(D) bind / method values

JS

var f = boo.foo.bind(boo2); // 当调用f()时,this将会指向boo2

Go

f := boo.foo // f(), is same as boo.foo()
f := boo.foo // f(), 与boo.foo()一样

(S) setTimeout / timer

JS

setTimeout(somefunction, 3*1000)

Go

time.AfterFunc(3*time.Second, somefunction)

(D) setInterval / ticker

JS

setInterval(somefunction, 3*1000)

Go

ticker := time.NewTicker(3 * time.Second)
go func() {
	for t := range ticker.C {
		somefunction()
	}
}()

(D) 字符串

JS

字符串的初始化使用的单引号('hello')或者双引号("hello"),但大多数编码风格更喜欢单引号,原生字符串文字使用反引号(`hello`)

Go

字符串的初始化使用双引号("hello")或者原生字符串文字使用反引号(`hello`)

(S) 注释

两个语言注释都一样 /* 多行注释 */  and // 行注释.

Variables

变量

(D) 值, 指针, 引用

在Javascript只有值与引用两类型,比如stringnumber是值类型。对象,数组,函数都是引用类型

在Go中,有值类型,引用类型和指针。 引用类型是slicesmapschannels。 其余的都是值类型,但也有“被引用”能力的指针。
在引用和指针之间要记住的最实际的区别是,虽然你可以使用两者来改变下层的值(当它是可变的),用指针也可以重新分配它。

JS

var a = {
    message: 'hello'
}

var b = a;

// mutate
b.message = 'goodbye';
console.log(a.message === b.message); // prints 'true'

// reassign
b = {
    message: 'galaxy'
}
console.log(a.message === b.message); // prints 'false'

Go

a := struct {
	message string
}{"hello"}

b := &a

// mutate
// note b.message is short for (*b).message
b.message = "goodbye"
fmt.Println(a.message == b.message) // prints "true"

// reassign
*b = struct {
	message string
}{"galaxy"}
fmt.Println(a.message == b.message) // prints "true"

流控制声明

(B) 轮询与迭代

For

JS

for(let i=0;i<10;i++){
    console.log(i);
}

Go

for i := 0; i < 10; i++ {
	fmt.Println(i)
}

While

在Go中,for的init和post语句是可选的,有效地使它成为“while”语句:

JS

var i=0;
while(i<10){
    console.log(i);
    i++;
}

Go

i := 0
for i < 10 {
	fmt.Println(i)
	i++
}

迭代Array/Slice

JS

['Rick','Morty','Beth','Summer','Jerry'].forEach(function(value,index){
    console.log(value + ' at index ' + index);
});

Go

for i, v := range []string{"Rick", "Morty", "Beth", "Summer", "Jerry"} {
	fmt.Printf("%v at index %d", v, i)
}

(B) If/Else

Go的if可以包含一个初始化语句,声明变量作用域范围仅是ifelse块的范围。

Go

if value := getSomeValue(); value < limit {
	return value
} else {
	return value / 2
}

(D) Switch

switch语句是编写此文档的动机之一。

Go默认情况下中断执行,否则需要fallthrough来声明继续执行。

Javascript默认情况下是穿透性的,否则就需要break中断执行。

JS

switch (favorite) {
    case "yellow":
        console.log("yellow");
        break;
    case "red":
        console.log("red");
    case "pruple":
        console.log("(and) purple");
    default:
        console.log("white");
}

Go

switch favorite {
case "yellow":
	fmt.Println("yellow")
case "red":
	fmt.Println("red")
	fallthrough
case "pruple":
	fmt.Println("(and) purple")
default:
	fmt.Println("white")
}

Functions

(S) 函数即是类

两种语言都把函数当作为类。 两者都允许函数作为参数传递,作为返回值,被嵌套,并且具有闭包。

Javascript中的函数嵌套可以使用命名和匿名函数,而在Go中,只能使用匿名函数。

(D) 多个返回值

Go函数可以返回多个值

Go

func hello() (string, string) {
	return "hello", "world"
}

func main() {
	a, b := hello()
	fmt.Println(a, b)
}

javascript不能返回多个值,但是可以通过解构赋值的语法获得类似行为

JS

function hello() {
  return ["hello", "world"];
}

var [a, b] = hello();
console.log(a,b);

(S) IIFE

即时调用函数表达式(英文:immediately-invoked function expression,缩写:IIFE)

JS

(function () {
  console.log('hello');
}());

Go

func main() {
	func() {
	    fmt.Println("Hello")
	}()
}

(S) 闭包 Closures

两个语言都有闭包,当在循环中创建闭包都需要谨慎creating closures inside loops。 以下是两种语言的示例,它展示了一种类似的技术来绕过闭合/循环陷阱:

JS (with bug)


var i = 0;
for (;  i < 10 ; i++) {
  setTimeout((function() {console.log(i);}),0);
}

JS (solved)

(注意可以使用for(let i=0; … 替代var来解决)

var i = 0;
for (;  i < 10 ; i++) {
  (function (i) {
    setTimeout((function() {console.log(i);}),0);
  }(i));
}

Go (with bug)

var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
	go func() {
		defer wg.Done()
		fmt.Println(i)
	}()
}
wg.Wait()

Go (solved)

var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
	go func(i int) {
		defer wg.Done()
		fmt.Println(i)
	}(i)
}
wg.Wait()

thank pazams