后端语言很难?前端入门go基础语法只需要3小时!(上)

lxf2023-03-13 08:53:01

前言

废话不多说,go基础语法,如果你前端基础稍微扎实一些,比如理解闭包,词法作用域,函数一等公民等等概念,了解go的基础语法只需要看完这篇文章,花费大概1小时。总共分为3篇(上中下)(共计3小时)。过年前全部出完,接着搞组件库了

本文是书籍<Go语言圣经>的浓缩版(我会用类比js语言里的东西去介绍go,难免不会百分百一样,但是会让你秒理解,所有类比js的内容会以"类比js"开头)

想要go资料的直接私信我,我发给你,包括书和视频,一起go!

入门

Hello,world

我们以现已成为传统的“hello world”案例来开始吧。这个例子体现了Go语言一些核心理念。

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令。Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。(本书使用$表示命令行提示符。)

类比js: js跟go不同,是一门解释型语言,虽然可能会在解释前做一些优化,叫做JIT,有兴趣的深入JIT的同学可以参考这篇文章(www.xttblog.com/?p=1962) 我举一个例子解释JIT。

JIT(Just-In-Time)即时编译技术是 JavaScript 引擎在运行时将代码转化为机器码的技术。JIT 可以提高 JavaScript 程序的运行效率,因为它可以避免在执行过程中重复地解释代码。

例如,以下代码在 JavaScript 中使用了 JIT 即时编译技术:

function add(a, b) {
  return a + b;
}

let result = add(1, 2);
console.log(result);  // Output: 3

在这个例子中,JavaScript 引擎在运行时将 add 函数编译为机器码。之后,当调用 add 函数时,引擎将使用编译后的机器码执行它,而不是重新解释代码。

这样可以提升性能,因为机器码的执行速度比解释代码的速度快得多。

继续go语言入门

$ go run helloworld.go

毫无意外,这个命令会输出:

Hello, 世界

Go语言原生支持Unicode,它可以处理全世界任何语言的文本。

类比js: js也是支持Unicode的,js的字符串在内部会解释为UTF-16,至于更深入的js和unicode以及utf编码的关系,请参考这篇文章(www.ruanyifeng.com/blog/2014/1…

如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以用build子命令:

$ go build helloworld.go

这个命令生成一个名为helloworld的可执行的二进制文件,之后你可以随时运行它,不需任何处理。

$ ./helloworld

Hello, 世界

Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。

类比js: 就是我们package.json里声明的name。

一个包由位于单个目录下的一个或多个.go源代码文件组成, 目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main, 表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如fmt包,就含有格式化输出、接收输入的函数。Println是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。

类比js: fmt类似console。

main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main 函数 也很特殊,它是整个程序执行时的入口。main函数所做的事情就是程序做的。当然了,main函数一般调用其它包里的函数完成很多工作, 比如fmt.Println

类比js:相当于我们src目录下的index.js或者index.ts,就是我们的入口文件。

必须告诉编译器源文件需要哪些包,这就是import声明以及随后的package声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。

类比js: 跟我们的import一样,导入包

必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包。

import声明必须跟在文件的package声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字func, var, const, type定义)。这些内容的声明顺序并不重要。这个例子的程序已经尽可能短了,只声明了一个函数, 其中只调用了一个其他函数。为了节省篇幅,有些时候, 示例程序会省略packageimport声明,但是,这些声明在源代码里有,并且必须得有才能编译。

一个函数的声明由func关键字、函数名、参数列表、返回值列表(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析。。举个例子, 函数的左括号{必须和func函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行。

类比js:我们大部分人提倡加分号是一个语句的结尾,其实js会把换行符默认加上分号(也有个别特殊情况,但是基本上可以忽略)

Go语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式,并且go工具中的fmt子命令会对指定包, 否则默认为当前目录, 中所有.go源文件应用gofmt命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。

类比js:相当于go自带格式化代码工具,我们通常通过IDE的插件,比如prettier

包管理

网上很多教程还是使用GOPATH来管理包,这种方式已经落后了,现在基本上都是用go mod去管理,跟我们前端的npm很像,初始化项目的方式如下(具体命令有不了解的同学可以去搜索一下以下命令是什么意思):

go mod init // 初始化项目目录
go build 编译项目,依赖库安装
go list -m list 查看当前项目依赖库
go get 追加更新项目的依赖库
go mod tidy 删除不必要的依赖库

程序结构

命名

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。

类比js:基本上是一致的,js也区分大小写。

Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。

break default func interface select

case defer go map struct

chan else goto package switch

const fallthrough if range type

continue for import return var

此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

类比js:不用纠结,js也有,不用记,谁会用let true =2;对吧,IDE也会提示你可能有问题

还有需要注意的有:

如果一个名字是在函数内部定义,那么它的就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的,那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。

  • 在习惯上,Go语言程序员推荐使用 驼峰式 命名。

声明

声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。

类比js:声明的变量和函数跟js一样,是有作用域的,我简单介绍一下。

在 Go 语言中,一个作用域内的变量可以被同一作用域内的其他变量访问,也可以被其外层作用域的变量访问。一个变量被定义在一个作用域中,则它可以被该作用域内和外的代码访问,但不能被其他作用域内的代码访问。这种访问顺序被称为作用域链。

变量

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:

var 变量名字 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

类比js: 我们基本上用let 来定义变量,而且跟ts很类似,会有自动的类型推断,需要主要的是,js中如果对一个变量只声明而不赋值,会默认赋值为undefined,而go不是。

例如

var s string

fmt.Println(s) // "" ,js的话,会是undefined

一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:

var f, err = os.Open(name) // os.Open returns a file and an error

类比js:这不就是结构赋值吗。。。我大js也有,还比这复杂的多呢,嵌套结构了解一下

再提一下go中的短变量声明:

在 Go 语言中,短变量声明是指在函数内部使用 := 符号简化变量声明的语法。这种方式只能在函数内部使用,不能在包级别使用。在使用短变量声明时,Go 会自动推断变量的类型,无需显式指定。

举个例子,这是使用短变量声明的方式声明变量 x 并赋值为 5 的语句:

x := 5

这与下面的语句等价:

var x int = 5

赋值

使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。

x = 1 // 命名变量的赋值

*p = true // 通过指针间接赋值

person.name = "bob" // 结构体字段赋值

count[x] = count[x] * scale // 数组、slice或map的元素赋值

特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:

count[x] *= scale

这样可以省去对变量表达式的重复计算。

类比js ,我们也有,比如 let a = 1; a *= 2;

数值变量也可以支持++递增和--递减语句。

类比js, 注意js有前++,前--,后++,后--,go没有前++,前--

type

在 Go 语言中,type 关键字用于定义新的数据类型。例如,可以使用 type 定义一个别名(即给已有类型取一个新名称),也可以使用它来定义结构体、接口等自定义类型。示例如下:

type MyInt int //MyInt是int的别名

type Person struct {
    name string
    age  int
}

type Myinterface interface{
  method1()
  method2()
}

这样MyInt就是int的别名, Person就是一个结构体类型,Myinterface就是一个接口类型。

通过定义自己的类型,可以让你对程序的某部分进行封装,控制这部分可以使用的操作。

类比js,这不就是ts里的type吗。。。很好理解吧

包和文件

Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中。

每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。

类比js,这个很好理解吧,就跟我们js的话,就是你发布到npm上的一个包,别的项目下载下来就可以引用了。

作用域

go和js都是词法作用域,不了解词法作用域的同学可以去搜索一下。

我简单说一下有哪些作用域:

在 Go 中,有三种不同的变量作用域:

  • 包级作用域:包级变量在整个包中都是可见的。当一个变量定义在文件顶部(不在任何函数内)时,它将具有包级作用域。
  • 函数级作用域:函数级变量只在定义它的函数内可见。当一个变量定义在函数内部时,它将具有函数级作用域。
  • 块级作用域:块级变量在一个块中可见,块指的是for loop, if等等。

注意,变量的作用域并不会跨越函数或包边界。包级变量只能在同一包中访问,函数级变量只能在定义它的函数中访问。

如果一个变量同时定义在块级和函数级或者包级作用域中,块级变量会覆盖函数级和包级变量。

Go 也有一个关键字 ":= " 可以用来声明并初始化局部变量,在函数体内部和块内部缩短变量的声明。

基础数据类型

整型

Go语言中支持以下整型类型:

  • int8、int16、int32、int64:有符号整型,分别对应8位、16位、32位、64位二进制补码。
  • uint8、uint16、uint32、uint64:无符号整型,分别对应8位、16位、32位、64位二进制原码。
  • int:与系统相关的有符号整型,在32位系统上等价于int32,在64位系统上等价于int64。
  • uint:与系统相关的无符号整型,在32位系统上等价于uint32,在64位系统上等价于uint64。

在Go语言中,默认使用int类型表示整数,并且可以使用类似C语言的类型转换运算符来转换不同类型的整数。

类比js,js现在新的标准也是支持Int8,unit8这种有符号,无符号的类型数组的。详情请见(developer.mozilla.org/zh-CN/docs/…

这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint;其中int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit,但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。

运算符优先级

Go语言中的运算符优先级按照以下顺序:

优先级越大,先计算

后端语言很难?前端入门go基础语法只需要3小时!(上)

接下来类比一下js的运算符优先级

后端语言很难?前端入门go基础语法只需要3小时!(上)

我的感觉是差不多,欢迎大家评论区评价。继续go。

一般来说,需要一个显式的转换将一个值从一种类型转化位另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。

在很多场景,会遇到类似下面的代码通用的错误:

var apples int32 = 1

var oranges int16 = 2

var compote int = apples + oranges // compile error

当尝试编译这三个语句时,将产生一个错误信息:

invalid operation: apples + oranges (mismatched types int32 and int16)

这种类型不匹配的问题可以有几种不同的方法修复,最常见方法是将它们都显式转型为一个常见类型:

var compote = int(apples) + int(oranges)

许多整形数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度:

f := 3.141 // a float64

i := int(f)

fmt.Println(f, i) // "3.141 3"

f = 1.99

fmt.Println(int(f)) // "1"

类比js,js中通常是隐式类型转换,我们是动态的语言嘛,隐式转换规则还挺多的,也是以前常见的面试题,现在好像这类面试题变少了,比如 [] + {}等于什么。有兴趣的参考(Admin.net/post/684490…

浮点数

Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大。

在 Go 语言中,有两种类型的浮点数: float32 和 float64。

float32 类型对应单精度浮点数,精度为 7 位小数,占用 4 个字节的存储空间。float64 类型对应双精度浮点数,精度为 15 位小数,占用 8 个字节的存储空间。

在程序中定义浮点数可以用如下语法:

var x float32 = 3.14
var y float64 = 3.14159265358979323846

对比js,js是采用的64位浮点数,所以我们经验还是可以复用到Go语言里的。但是你要彻底理解浮点数在计算机底层的存储原理,可以参考这篇文章(baijiahao.baidu.com/s?id=167926…

推荐大家把浮点数的知识点搞清楚,比如说你知道单精度浮点数和双精度浮点数的有效小数位是几位吗,这就意味着,他们最大的整数就是这个位数?有点懵是不是,哈哈,没关系,多看几次就好了,因为后端对数据大小是很敏感的,因为涉及到存储,前端可能没那么敏感。

布尔型

这个没啥好说的,跟js一样,true和false

字符串

一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列,

类比js:js的字符串内部使用的UTF-16表示,我们跟go一样也是字符串是不可变的字符序列。在go里rune类型能表示一个字符,前端没有对应的方式去表示。可能正则算一个吧,用/.*/ug来匹配任意字符

上面提到的/.*/ug这个正则表达式里的u你一定要明白为什么使用,详情请见es6.ruanyifeng.com/#docs/regex…

继续go的内容。

内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。

s := "hello, world"

fmt.Println(len(s)) // "12"

fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')

如果试图访问超出字符串索引范围的字节将会导致panic异常:

c := s[len(s)] // panic: index out of range

第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。我们先简单说下字符的工作方式。

子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节。

fmt.Println(s[0:5]) // "hello"

同样,如果索引超出字符串范围或者j小于i的话将导致panic异常。

不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。

fmt.Println(s[:5]) // "hello"

fmt.Println(s[7:]) // "world"

fmt.Println(s[:]) // "hello, world"

其中+操作符将两个字符串链接构造一个新字符串:

fmt.Println("goodbye" + s[5:]) // "goodbye, world"

字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

s := "left foot"

t := s

s += ", right foot"

这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。

fmt.Println(s) // "left foot, right foot"

fmt.Println(t) // "left foot"

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

s[0] = 'L' // compile error: cannot assign to s[0]

常量

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。

对比js,在js里常量可以是对象,但是go里面不可以

一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。

所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof。

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:

const (
    a = 1
    b
    c = 2
    d
)

fmt.Println(a, b, c, d) // "1 1 2 2"

如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。

复合数据类型

for循环

讲数组之前我们看看go的for循环怎么实现。

上面可以看到,有一个for循环语句,我们来看下go里的for循环:

在 Go 语言中,for 循环语句是一种基本的循环结构,用于重复执行一组语句。Go 语言支持三种不同的形式的 for 循环语句,分别为:

  • 标准形式:
for init; condition; post {
    // 循环体
}
  • 省略初始化和增量表达式的形式:
for condition {
    // 循环体
}
  • 无限循环的形式:
for {
    // 循环体
}

在标准形式的 for 循环中,init 是在循环开始之前执行的语句,通常用来定义和初始化循环变量;condition 是循环条件,循环会持续执行直到条件为 false;post 是在每次循环结束后执行的语句,通常用来增量或者更新循环变量。

省略初始化和增量表达式的形式 和 无限循环的形式 都是基于标准形式的一种简化,省略了初始化语句和增量语句。

在循环体中使用 "break" 可以终止循环,使用 "continue" 可以跳过当前循环并继续下一次循环。

for i := 0; i < 5; i++ {
    if i == 2 {
        break
    }
    if i == 1 {
        continue
    }
    fmt.Println(i)
}

输出结果是:

0

这是因为在i=2时终止循环,i=1

对比js,感觉没啥区别,就是没有for后面没有了括号。

但是还有一种叫for range语法遍历数组(对象也可以),我们简单了解一下

for range 是 Go 中的一种循环语句,用于遍历数组、切片、字符串、集合或者 map 的元素。它有如下的语法结构:

for key, value := range collection {
    // do something with key and value
}

在遍历过程中,range 会返回每个元素的索引(或键)和值。如果你不需要索引,可以使用下划线来代替:

for _, value := range collection {
    // do something with value
}

遍历数组的例子

arr := [3]int{1, 2, 3}
for i, v := range arr {
    fmt.Println("index:", i, "value:", v)
}

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

var a [3]int             // array of 3 integers
fmt.Println(a[0])        // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]

// Print the indices and elements.
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

// Print the elements only.
for _, v := range a {
    fmt.Printf("%d\n", v)
}

对比js,js里的数组是啥类型都可以往里装(有兴趣的同学可以去搜一下快数组和慢数组,这是v8引擎在js内部实现数组的方式,你带着一个问题去阅读更好,就是v8引擎如何实现数组的动态扩容,go里的数组是静态的,开始说了只有10个元素就不能变了),然后go里的数组要求数据类型一致,在js里面不要求

默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组:

var q [3]int = [3]int{1, 2, 3}

var r [3]int = [3]int{1, 2}

fmt.Println(r[2]) // "0"

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为

q := [...]int{1, 2, 3}

fmt.Printf("%T\n", q) // "[3]int"

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

q := [3]int{1, 2, 3}

q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int

js类比:其实js的数组跟后面我们讲的go里面的切片很像,但是无论是go的切片还是js的数组,其实都是一个效率比较低的数据结构,你了解v8引擎的快数组就会明显,跟go是一样的,扩容方式是去内存开辟一片更长的连续内存空间,把当前数组的元素拷贝过去,然后剩下的空间就是扩容后新增的

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。

a := [2]int{1, 2}

b := [...]int{1, 2}

c := [2]int{1, 3}

fmt.Println(a == b, a == c, b == c) // "true false false"

d := [3]int{1, 2}

fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

js类比:js比较数组是判断两者的指针是否一样,也就是引用是否一样,而go里面却不是,比较的是两个数组的元素是否一样(go里的数组只能存储基本类型,不存在存引用类型,按go的话说,必须是go的常量)

下面注意一个go跟js非常不同的地方,就是数组作为参数传递给函数,go居然是传递了一个副本,就是复制了一份给函数,然后不会影响原先数组的数据。js是传递引用进去的.比如

var a = [1,2]

var b = (v) => v[0] = 33;
b(a)

console.log(a); [33, 2]

但是go的话,a数组不会受到影响,牛b吧,这会造成数组作为参数很低效,要复制

如何避免这个问题,我们可以往函数里传的是指针,而不是数组就好了,此时的行为就跟js一致了。

func zero(ptr *[32]byte) {
    *ptr = [32]byte{}
}

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。

其他:

后端语言很难?前端入门go基础语法只需要3小时!(下)