这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
1.前言
我之前都是写Java,由于参加了青训营而开始学习go。但是从个人观点来说,语言只是工具,所以只需要学学语法和语言特性就好了,于是便开始随着课程的进度学习。
正所谓好记性不如敲烂键盘,学过的东西还是需要沉淀和分享,这样更有助于成长,于是就简单记录了一下在学习过程中的思考,我会将go和java的类比,同时引入一些go的设计理念✨。
2.简介
Go,起源于2007年,并在2009年正式对外发布,其实都是Google的,设计Go语言的初衷都是为了满足Google的需求。Go的主要目标是“兼具Python等动态语言的开发速度和C/C++等编译型语言的性能与安全性🚄”,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势。最主要还是为了并发而生,并发是基于goroutine的,goroutine类似于线程,但并非线程,可以将goroutine理解为一种虚拟线程。Go语言运行时会参与调度goroutine,并将goroutine合理地分配到每个CPU中,最大限度地使用CPU性能。协程这个概念的引入给的最大的感受就是比java快,web部署比java简单。
3.环境
和java需要下载JDK,配置环境变量一样。Go也需要下载GO,里面提供了各种developkit,library以及编译器。在官网下载后直接安装。在终端中输入go env验证是否安装成功并且查看go版本,GOPATH,GOROOT等参数。
具体安装过程和环境变量不再表述,直接搜索即可😜。
GOROOT和GOPATH的区别
-
GOROOT:表示的是Go语言编译、工具、标准库等的安装路径,其实就相当于配置JAVA_HOME那样。就是自己安装好go的那个目录
-
GOPATH:和java不同,java里没有这个变量。这个事表示Go的工作目录,是全局v的,当执行Go命令时会依赖这个目录。一般还会把$GOPATH/bin设置到PATH目录,这样编译过的代码就可以直接执行了。
注意:GOROOT路径和GOPATH路径不能相同
GoLand
自动import,超爽的体验👍!我强烈支持只使用过IDEA的选手用GoLand,操作逻辑和IDEA一致,十分钟上手写代码!!但是注意GoLand版本是否支持当前的GO版本,go版本太高就要回退😢
运行项目需要设置build config,和Android、Java的都差不多,例如创建一个hello-goland项目:
导入go module项目的时候需要勾选这项,否则无法像maven/gradle那样sync下载依赖:
VScode
搜索go插件,第一个安装量最多的就是,可能会下载不来,可以用换下载源(七牛云)。具体百度就行。
工程目录
在设置GOPATH环境变量的时候,这个目录里面又分了三个子目录bin、pkg、src,分别用于存放可执行文件、包文件和源码文件。当我们执行Go命令的时候,如果我们指定的不是当前目录的文件或者绝对路径的目录的话,就会去GOPATH目录的去找。这样在GOPATH目录创建了xxx的目录后,就可以在任意地方执行 go build xx 命令来构建或者运行了。
├── bin
├── pkg
└── src
pkg目录应该是在执行 go install 后生成的包文件,包括.a这样的文件,相当于一个归档。
另外,通过IDE可以设置project的GOPATH,相当于在执行的时候给GOPATH增加了一个目录变量,也就是说,我们创建一个项目,然后里面也有bin、src、pkg这三个目录,和GOPATH一样的,本质上,IDE在运行的时候其实就是设置了一下GOPATH:
GOPATH=/Users/fuxing/develop/testgo/calc-outside:/Users/fuxing/develop/go #gosetup
Go语言在寻找变量、函数、类属性及方法的时候,会先查看GOPATH这个系统环境变量,然后根据该变量配置的路径列表依次去对应路径下的src目录下根据包名查找对应的目录,如果对应目录存在,则再到该目录下查找对应的变量、函数、类属性和方法。
GoModules
从1.13版本开始,更是默认开启了对Go Modules的支持,使用Go Modules的好处是显而易见的 —— 不需要再依赖GOPATH,你可以在任何位置创建Go项目,并且在国内,可以通过 GOPROXY 配置镜像源加速依赖包的下载。也就是说,创建一个项目就是一个mod,基本上目前Go开源项目都是这样做的。其实就是类似于Maven和Gradle。 😍
// 创建mod项目,也是可以用IDE来new一个mod项目的:
go mod init calc-mod
// 一般开源在github上面的项目名字是这样的;和maven、gradle不一样的是,开发完成根本不需要发布到仓库!只要提交代码后打tag就可以了
go mod init github.com/fuxing-repo/fuxing-module-name
// 创建一个模块:执行这个命令主要是多了一个go.mod文件,里面就一行内容:
module calc-mod
// import以后,执行下载依赖命令,不需要编辑go.mod文件。依赖会下载到GOPATH/pkg/mod目录
go list
语法
由于篇幅原因这里只讲基础的关键字
包
Java里面的包名一般是很长的,和文件夹名称对应,作用就是命名空间,引入的时候需要写长长的一串,也可以用通配符
Go里面一般的包名是当前的文件夹名称,同一个项目里面,可以存在同样的包名,如果同时都需要引用同样包名的时候,就可以用alias区分,类似于JS那样。一般import的是一个包,不像Java那样import具体的类。同一个包内,不同文件,但是里面的东西是可以使用的,不需要import。这有点类似于C的include吧。如果多行的话,用括号换行包起来。
在java里只有静态或者对象可以使用.操作符。在go里可以用一个包名.,结合import来使用,可以点出一个函数调用,可以点出一个结构体,一个接口,
区别于C,不管是指针地址,还是对象引用,都是用点运算符,不需要考虑用点还是箭头了!
程序的入口的package必须是main包,和java一样需要main函数。
变量
与java不同,类型是放在变量名后。
变量可以通过赋值推导类型,并且变量自带初始值
var 变量名字 类型 = 表达式
//一次声明多个变量
var (...)
var v1 int = 10 // 方式一,常规的初始化操作
var v2 = 10 // 方式二,此时变量类型会被编译器自动推导出来
v3 := 10 // 方式三,可以省略 var,编译器可以自动推导出v3的类型
多重赋值: 交换i和j
i, j = j, i
匿名变量
用 _ 来表示,作用就是可以避免创建定义一些无意义的变量,还有就是不会分配内存。
一般是调用一个函数时,这个函数有两个返回值a,b。我们不需要返回值b,就可以用 _ 代替。
变量类型
Go语言内置对以下这些基本数据类型的支持:
- 布尔类型:bool
- 整型:int8、byte、int16、int、uint、uintptr 等
- 浮点类型:float32、float64
- 复数类型:complex64、complex128
- 字符串:string
- 字符类型:rune,本质上是uint32
- 错误类型:error
此外,Go语言也支持以下这些复合类型:
- 指针(pointer)
- 数组(array)
- 切片(slice)
- 字典(map)
- 通道(chan)
- 结构体(struct)
- 接口(interface)
常量 const
还有const常量,iota这个预定义常量用来定义枚举。可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。
if else
和java相比,表达式的地方是没有小括号的,但是对应的代码要包裹在大括号中
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
// if上还可以声明变量
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {。。。}
for
和java相比只有for循环,没有while,do/while,判断条件处条件无括号
for {
fmt.Println("loop")
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
switch/case/default
不用break跳出case。
switch不用变量,可以直接在case中写判断。这样可以取代if/else,使得程序更加简介
a :=1
switch a {
case 1:
fmt.Println("one")
。。。
default:
fmt.Println("other")
}
可以通过case上直接写表达式来判断,简化if
switch {
case t.Hour() < 12:
fmt.Println("It's morining")
case t.Hour() >= 12:
fmt.Println("It's afternoon")
}
字典
就是java中的map,演示创建和遍历
注意:不能对map做取地址操作
var testMap map[string]
inttestMap = map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
// 遍历map 顺序不确定
for s, i := range m2{
fmt.Printf("%s\t%d\n",s, i)
}
函数
格式:func (结构体名) funcName (变量名 变量类型)(返回类型){}
func add(a int, b int) int {
return a + b
}
数组和切片的区别
在学习过程中发现数组和数组和切片非常相似,在声明和使用的时候也很容易混淆。
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列,slice功能也更灵活。
切片
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
区别一
- 初始化
//初始化数组
a := [3]int{1,2,3} //指定长度
a := [...]int{1,2,3} //不指定长度
//初始化切片
s := make([]int, 3) //指定长度
s := []int{1,2,3} //不指定长度
- 切片不只有长度(len)的概念,同时还有容量(cap)的概念。因此切片其实还有一个指定长度和容量的初始化方式:
s := make([]int, 3, 5)
- 此外,切片还可以从一个数组中初始化(可应用于如何将数组转换成切片):通过数组 a 初始化了一个切片 s。
a := [3]int{1,2,3}
s := a[:]
如果将 append 用在数组上,你将会收到报错:first argument to append must be slice。
区别二:函数传递
当切片和数组作为参数在函数(func)中传递时,数组传递的是值,而切片传递的是指针。因此当传入的切片在函数中被改变时,函数外的切片也会同时改变。相同的情况,函数外的数组则不会发生任何变化。
知道的越多,不知道的也越多。第一次写文章,欢迎大家指正。