Go语言基础语法 part 1 | 青训营笔记

107 阅读8分钟

这篇笔记是个人对青训营课程的笔记,也夹杂了一些自己这段看到的零碎资料。包括 Effective GoGo in Action 什么的,就不一一列举出处了,讲到哪里算哪里

1.1 什么是 Go 语言

Go 语言是谷歌公司出品的一门编程语言。

有一个开玩笑的说法,是因为谷歌内部的 C++ 构建系统太慢了,几位大佬在上班摸鱼等待项目编译的时候提出要整一个编译更快,更适合工程的编程语言。然后,Go 语言诞生了。

顺便一提: 上面说的大佬真的是严格意义的大佬(比如:Unix 发明者,UTF-8 发明者,曾负责研发V8引擎的...),有兴趣可以自行查阅,这里不做赘述了。

大家搜 Go 语言英文资料的时候要记住,Go 语言的英文就是 Go,而不是 Golang,golang 是早期域名,并非语言本身的名字,但是为了能够搜索出有效的结果,搜索框里还是得输入 golang

还有,为什么 Go 的域名不是 .com 的?因为这个域名被某个版权领域令人闻风丧胆的公司占了

1.2 Go 语言的特点

  • 高性能,高并发:Go 提供了原生的并发支持
  • 语法简单易懂:类似C,并大幅简化,仅有25个关键字
  • 丰富的标准库:Go 拥有和 Python 类似的丰富的标准库,能保证高质量和持续优化
  • 完善的工具链:Go 自带了编译,代码格式化,依赖管理,文档,代码测试等一系列工具。
  • 静态链接:Go 语言默认是静态编译,编译结果是一个单一的可执行文件,可直接运行
  • 快速编译:Go 的编译速度很快,可以告别编译时的长时间等待摸鱼
  • 跨平台:Go 的官方工具链就支持交叉编译,不管是不同的体系结构还是操作系统,都很容易生成目标平台的二进制文件(你甚至可以让你的代码在 Plan 9 From Bell Labs 上运行)
  • 垃圾回收:垃圾回收的确会带来额外的性能开销,但其优点是大幅度减轻了开发人员的心智负担,还能防脱发

1.3 & 1.4 Go 的应用

Go 语言有哪些企业用呢,这里就不照着念列表了,反正招聘网站上挺好了解的,说说我自己的感受。

虽然学校没有教过 Go,在参加青训营之前我也没有自学过,但这个语言还是给我印象蛮深的。

主要是因为,每次一旦某个视频站的服务崩了,就总会有人在技术群转发一篇 《X站高可用构架实践》 的文。所以某个站换 Go,这个大家如果对这方面有所关注的应该都知道了。

字节跳动是全面拥抱 Go,青训营老师说原因是早期字节的业务是 Python 实现的,业务体量大了以后就遇到了性能问题,Cpp 不适合 Web 开发,早期技术团队又没有 Java 背景,公司内小范围使用 Go 以后发觉不错,就转而推广开来。

2 开发

2.1 IDE

可以选择开箱即用的 Goland,也可以选择自行配置 VSCode

2.2 基础语法:HelloWorld

注释很详细了,需要注意的是 Go 语言的分号是真的可以不写的(通常 JavaScript 有的最佳实践会建议虽然可以不写,但建议无论如何都写上分号)

package main // 文件是 main 包的一部分, main 包是程序的入口包,说明该文件是程序的入口文件

import (
	"fmt" //导入标准库中的 fmt 包,用于向屏幕输入输出字符串和格式化字符串
)

func main() { // main 函数
	fmt.Println("hello world") // 打印 hello world
}

你可以直接运行

go run main.go

也可以编译出二进制文件再运行:

go build main.go
./main

这里要注意,只有 main 包才能生成可执行文件。

类型

package main

import (
	"fmt"
)

func main() {
	var a = "initial"   // go 的变量声明: var ]变量名] = [值],变量类型会自动推导
	var b, c int = 1, 2 // 显式声明变量类型

	var d = true
	var e float64

	f := float32(e) // 另一种声明变量类型的方法 [变量名] := [值]
	g := a + "foo"

	fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
	fmt.Println(g)                // initial apple

	const s string = "constant" // 常量的声明就是将 var 关键字改为 const,go 的常量没有确定的类型,会根据使用的上下文自动确定类型。
	const h = 500000000
	const i = 3e20 / h

	fmt.Println(s, h, i)
}


Go 语言是一门强类型语言,你可能看到这个 var 觉得很像 JavaScript ?确实,类型不用显式声明,编译器会自动推断,但 Go 编译器会严格检查变量类型,不会像 JavaScript 那样,允许一些,嗯,看起来蛮神奇的隐式类型转换。

这里面的 :=在 Go 语言中被称为短变量声明,如果你学过Python,,python在3.8 以后有个一模一样的运算符(虽然功能可能不完全一样),它有一个很形象的别名叫做海象运算符(海象的黑眼睛和大牙齿)

2.3 基础语法 if else

package main

import "fmt"

func main() {
	if 7%2 == 0 { // go 语言中 if 后没有括号,写了括号在保存时会被编辑器自动去掉,此外,if 后必须跟大括号,不能像 C/C++ 把 if 里面的语句写到同一行
		fmt.Println("7 is even")
	} else {
		fmt.Println("7 is odd")
	}

	if 8%4 == 0 {
		fmt.Println("8 is divisable by 4")
	}

	if num := 9; num < 0 {
		fmt.Println(num, "is negative")
	} else if num < 10 {
		fmt.Println(num, "has 1 digit")
	} else {
		fmt.Println(num, "has multiple digits")
	}
}

2.4 循环

Go 语言只有一种循环:for循环 你同样可以使用 break 和 continue 语句来跳出循环。

不带条件的 for 循环就是死循环

单个条件就是 while 循环

三个条件就是标准的for循环了

条件都不需要写括号

package main

import "fmt"

func main() {
	i := 1
	for {
		fmt.Println("loop")
		break
	}
	for j := 7; j < 9; j++ {
		fmt.Println(j)
	}
	for n := 0; n < 5; n++ {
		if n%2 == 0 {
			continue
		}
		fmt.Println(n)
	}

	for i <= 3 {
		fmt.Println(i)
		i = i + 1
	}

}

2.5 基础语法 switch

如果你之前学过 C 语言,可能会记得 switch 语句有一个特性,如果一个 case 不加 break 的话,会从这条 case 一直往下,把之后所有的 case 执行一边,这个特性你可能还用过,比如计算某天是某一年的第几天。在 Go 语言中,没有这个特性。

package main

import (
	"fmt"
	"time"
)

func main() {
	a := 2
	switch a {
	case 1:
		fmt.Println("one") // 不加 break,也只会执行一次case
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	case 4, 5:
		fmt.Println("four or five")
	default:
		fmt.Println("other")
	}

	t := time.Now()

	switch {
	case t.Hour() < 12:
		fmt.Println("It's before noon")
	default:
		fmt.Println("It's after noon")
	}
}

2.6 基础语法:数组

在实际应用中,使用数组的场景相对较少,通常是使用切片而不是数组,但其实数组是切片的底层实现。那数组的底层又是什么呢?是连续分配的内存。

数组本身是一维的,但是可以通过数组的组合创建多维数组,代码中就有二维数组。twoD[i][j]

package main

import "fmt"

func main() {

	var a [5]int
	a[4] = 100
	fmt.Println(a[4], len(a))

	var twoD [2][3]int
	for i := 0; i < 2; i++ {
		for j := 0; j < 3; j++ {
			twoD[i][j] = i + j
		}
	}
	fmt.Println("2d: ", twoD)
}

2.7 基础语法:切片

之前已经说了在实际业务中很少使用数组,而是使用切片,原因之一是切片更灵活,切片,确实可以切一部分出来(返回一个新的slice),同时也可以通过 append 方法很方便的动态增长(扩容)。如果新的数组容量超过了原先的容量,扩容实际上是申请了一个新的底层数组,并把值复制进去。

另一个原因是性能问题,这是我在 Go in Action 上看到的,如果将一个很长(比如,100万个int类型元素)以传值的形式作为函数调用,在 64 位机器上,这需要 8MB 内存。是每次函数调用都会在栈上分配 8MB 内存,从而能够复制数组的值,如果是传指针,则只需要 8 字节,但指针的操作复杂,使用切片能更好解决这类共享问题,按值传递切片只需要 24 个字节,包含 8 字节指针,8字节长度,8字节容量。这大大降低了开销(注意传切片同样是两个函数共享相同的底层数组。)。

package main

import "fmt"

func main() {
	s := make([]string, 3)
	s[0] = "a"
	s[1] = "b"
	s[2] = "c"
	fmt.Println("get:", s[2])
	fmt.Println("len:", len(s))

	s = append(s, "d")
	s = append(s, "e", "f")
	fmt.Println(s) // [a b c d e f]

	c := make([]string, len(s))
	copy(c, s)
	fmt.Println(c) // [a b c d e f]

	fmt.Println(s[2:5]) // [c d e] 取出第二到第五个元素,不包括第五个元素,不同于 python,不支持负数索引,需要用len取出slice的长度再做一些简单的运算
	fmt.Println(s[:5])  // [a b c d e]
	fmt.Println(s[2:])  // [c d e f]

	good := []string("g", "o", "o", "d")
	fmt.Println(good) // [g o o d]
}

part1 部分的笔记就到这里,剩下部分见 part 2