Go语言入门分享 | 青训营笔记

141 阅读10分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第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等参数。

具体安装过程和环境变量不再表述,直接搜索即可😜。

image-20220510160747137.png

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项目: image-20220510161645953.png

导入go module项目的时候需要勾选这项,否则无法像maven/gradle那样sync下载依赖: image-20220510161822883.png

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吧。如果多行的话,用括号换行包起来。

image-20220510162555560-16521711570077.png

在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,就可以用 _ 代替。

image-20220510170045428-16521732468998.png

变量类型

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)中传递时,数组传递的是值,而切片传递的是指针。因此当传入的切片在函数中被改变时,函数外的切片也会同时改变。相同的情况,函数外的数组则不会发生任何变化。

知道的越多,不知道的也越多。第一次写文章,欢迎大家指正。