Go在vscode中的配置以及运行

通过参考Go安装中(1)(2),以及注意基本能够完成配置。

go的运行可以通过go run xxx.go来简单地完成。

go run命令的相关注意:

  1. 可以在go run后面追加命令行参数,这部分参数会作为代码可以接受的命令行输入提供给程序。

  2. 该命令会编译源码,并且直接执行源码的 main() 函数,不会在当前目录留下可执行文件,可执行文件被放在临时文件中被执行。

  3. go run不能使用go run+包的方式进行编译,如需要快速编译,可以用下面步骤来代替:

    1
    2
    1,使用go build生成可执行文件。
    2,运行可执行文件。

Go语法速记

基础

包、变量和函数

每个Go程序都是由包构成的。程序从main包开始运行。

能够通过import导入外部包,可以单独导入,也能以圆括号()的方式进行分组导入。按照规定,包名与导入路径的最后一个元素一致。例如,“math/rand”包中的源码均以package rand语句开始。以一个程序为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"math/rand"
)

func add(x int, y int) int {
return x+y
}

func main(){
fmt.Println("My favorite number is", rand.Intn(10))
}

导出名

观察到rand.Intn()Intn方法首字母进行了大写,这是导出名的规范。在Go中,如果一个名字以大写字母开头,那么它就是已导出的(例如,math.Pi)。在导入一个包时,只能引用其中已导出的名字,任何“未导出”的名字在该包外均无法访问。

函数

  • 【函数形参】对比addmain函数,可知函数可以没有参数或接受多个参数;参数列表中,变量的类型在变量名之后。这样的声明方式有点奇怪,这篇文章中解释了这种类型声明方式出现的原因(Go’s Declaration Syntax - Go 语言博客 (go-zh.org))。同时,连续的相同类型的形参,除最后一个外,它们的类型可以省略(如“x int, y int”->”x,y int”)

  • 【函数返回值】go中的函数可以返回任意数量的返回值,例如swap返回了两个字符串:

    1
    2
    3
    func swap(x, y string)(string, string){
    return y, x
    }
  • 【命名返回值】go函数的返回值可以被命名,它们会被视为定义在函数顶部的变量。如下例,没有参数的return语句会返回已命名的返回值。返回值的名称应该有一定含义,以便阅读,且命名返回值应该仅用于小函数,否则会影响代码的可读性。

    1
    2
    3
    4
    5
    func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
    }

变量

  • 【变量声明】var语句用于声明一个变量列表,同样,类型放在最后。var语句可以出现在包或函数级别。

  • 【变量初始化】变量声明中可以包含初始值,每个变量对应一个。如果初始化值已存在,那么就可以省略类型,变量会从初始值中获得类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var c, python bool

    var i, j int = 1, 2
    var c, python, java = true, false, "no!"

    func test(k int) int {
    k:=3
    return
    }
  • 【短变量声明】在函数中,可以使用简洁赋值语句“:=”来在类型明确的地方代替var声明。注意,函数外的每个语句都必须以关键词开始(var, func等),因此”:=”不能在函数外使用。

  • 没有初始化的变量声明,会为变量赋予零值。数值类型为0,布尔类型为false,字符串为""

基本类型

go中的基本类型有如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

float32 float64

complex64 complex128

同导入语句一样,变量声明也可以“分组”成一个语法块:

1
2
3
4
5
6
7
8
9
10
import (
"fmt"
"math/cmplx"
)

var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。

类型转换

表达式T(v)将值v的类型转化为T。与C不同的是,go在不同类型的项之间赋值时需要显式转换,否则将会报错。一些类型转换的例子:

1
2
3
4
5
6
7
8
func main(){
var i int = 42
var f float64 = float64(i)
u := uint(f) // 注意,这句只能在函数中
// Printf中的%T可以获取变量的类型
fmt.Printf("f is of type %T\n", f)
fmt.Printf("u is of type %T\n", u)
}

常量

常量使用const关键词声明,且不能用”:=”语法声明。

1
const Pi = 3.14159

流程控制语句:for、if、switch和defer

for

go中只有一种循环结构,即for循环。基本for循环由三部分构成:初始化语句,条件表达式,后置语句。初始化语句通常为一句短变量声明,该变量在for作用域中可见。

1
2
3
4
5
6
7
func main(){
sum:=0
for i:=0; i<10; i++{
sum += i
}
fmt.Println(sum)
}

注意:go的for语句三个构成部分外没有小括号()大括号{}则是必须的。

与C一样,初始化语句和后置语句是可以省略的。若这两个部分省略了,此时的for就相当于while,可以这么描述:

1
2
3
4
sum:=1
for sum<1000{
sum+=sum
}

如果再省略条件表达式,那么就会构成一个无限循环语句块:

1
2
for {
}

if

if语句与for类似,表达式外没有小括号(),而需要大括号{}。if语句也可以在条件表达式之前执行一个简单的语句,该语句声明的变量作用域仅在if内:

1
2
3
4
5
6
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}

练习

为了掌握forif语句,现在来做一个practise:用牛顿法实现平方根函数。

计算机通常使用循环来计算 x 的平方根。从某个猜测的值 z 开始,我们可以根据 z² 与 x 的近似度来调整 z,产生一个更好的猜测:

1
z -= (z*z - x) / (2*z)

重复调整的过程,猜测的结果会越来越精确,得到的答案也会尽可能接近实际的平方根。

在提供的 func Sqrt 中实现它。无论输入是什么,对 z 的一个恰当的猜测为 1。 要开始,请重复计算 10 次并随之打印每次的 z 值。观察对于不同的值 x(1、2、3 …), 你得到的答案是如何逼近结果的,猜测提升的速度有多快。

提示:用类型转换或浮点数语法来声明并初始化一个浮点数值:

1
2
z := 1.0
z := float64(1)

我的一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"math"
)

func Sqrt(x float64) float64 {
z := float64(1)
diff := float64(0.000000001)
for math.Abs(z*z-x)>diff {
z -= (z*z - x) / (2*z)
//fmt.Println(z)
}
return z
}

func main() {
fmt.Println(Sqrt(2))
}

switch

go的switch语句与C类似,但是go只运行选定的case,而非之后所有的case,即go自动提供了每个case后面的break语句(除非以fallthrough语句结束,否则分支会自动终止)。另一个重要的不同在于go中的switch无需为常量,且取值不必为整数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"runtime"
)

func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}

switch顺序地从上往下匹配,直到匹配成功停止。

技巧:没有条件的switch同switch true一样,这种形式能将一长串if-then-else写的更加清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}

defer

defer语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

在一个函数内,推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照LIFO的顺序调用。Defer 通常用于简化执行各种清理操作的功能。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()

dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()

return io.Copy(dst, src)
}

Defer 语句允许我们在打开每个文件后立即考虑关闭它,从而保证无论函数中的 return 语句数是多少,这些文件都将被关闭。

关于defer的更多信息,可以参考Defer,Panic, and Recover这篇文章。

更多类型:struct、slice和映射

指针

go具有指针,指针保存了值的内存地址。类型*T是指向T类型值的指针,其零值为nil

&操作符会生成一个指向其操作数的指针;

*操作符表示指针指向的底层值。

但是注意,与C不同的是,go没有指针运算。

1
2
3
4
5
6
7
8
9
10
11
func main() {
i, j := 42, 2701
p := &i // p初始化指向i
fmt.Println(*p)
*p = 21
fmt.Println(*p)

p = &j // p赋值,重定向到j
*p = *p/37
fmt.Println(*p)
}

结构体

一个结构体(struct)就是一组字段。结构体内部的字段使用点号”.”来访问,也可以通过结构体指针来访问(指针访问按道理应该(*p).X来访问字段X,但是这样太啰嗦了,所以也允许隐式间接引用,直接p.X)。

1
2
3
4
5
6
7
8
9
10
11
type XiaoBei struct{
Name string
Age int
}

func main() {
var bei XiaoBei = XiaoBei{"wlz",18}
bei.Age = 21
p := &bei
fmt.Printf("Name: %s Age: %d\n", bei.Name, p.Age)
}

【结构体文法】能够允许直接列出字段的值来新分配一个结构体,使用Name:语法可以仅列出部分字段(字段名的顺序无关)。

1
2
3
4
5
6
7
8
9
10
var (
v1 = Vertex{1,2} // 创建一个Vertex类型的结构体
v2 = Vertex{X:1} // Y:0被隐式地赋予
v3 = Vertex{} // X:0, Y:0
p = &Vertex{1,2} // 创建一个*Vertex类型的指针
)

func main() {
fmt.Println(v1, v2, v3, p)
}

数组

类型[n]T表示拥有nT类型值的数组。例如,var a [10]int 。这样的数组是固定大小的。

切片

切片为数组元素提供了动态大小的、灵活的视角。

类型[]T表示一个元素类型为T的切片。切片通过两个下标来界定,它是一个半开区间 [start, end)。

注意:切片就像数组的引用。切片本身并不存储任何数据,更改切片的元素会修改其底层数组中对应的元素。同时,与它共享的底层数组的切片都会观测到这些修改。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
names := [2]string {
"小🐖贝",
"小美贝",
}
fmt.Println(names)
a := names[0:2]
b := names[0:1]

a[0] = "pig"
fmt.Println(a,b,names)
}
  • 【切片文法】 []bool{true, true, false}会创建一个[3]bool数组,并构建一个引用了它的切片。

  • 【切片的默认行为】在进行切片时,可以利用它的默认行为来忽略上下界,切片下界默认为0,而上界是切片的长度。

  • 【切片的长度与容量】长度:切片所包含的元素个数(可以理解成视图),用len(s)来获取;容量:从切片的第一个元素开始数,到其底层数组元素末尾的个数(理解为切片的最大尺寸),用cap(s)来获取。

  • 【nil切片】切片的零值是nilnil切片的长度和容量为0,且没有底层数组。

  • 【用make创建切片】利用内建函数make来创建切片,这也是创建动态数组的方式。

    1
    2
    3
    a := make([]int,5)         // len(a)=cap(a)=5
    b := make([]int, 0, 5) // len(b)=0, cap(b)=5
    c := b[2:5] // len(c)=3, cap(c)=3
  • 【切片的切片】切片可以包含任何类型,甚至包括它的切片。也就是多维数组的形式。

  • 【向切片追加元素】内建函数append能够为切片追加新的元素。

    1
    func append(s []T, vs...T) []T

    第一个参数s,为类型T的切片;第二个参数vs,表示其他类型为T的值将追加到切片的末尾。

    当s的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组,返回的切片会指向这个新分配的数组。

  • 【More…】关于切片更多的内容,参考Go 切片:用法和本质 - Go 语言博客 (go-zh.org)

方法和接口

并发

参考

Go安装:

  1. Go语言的安装与配置-vscode篇 - 知乎 (zhihu.com)
  2. go mod问题

注意,在(2)完成后,vscode会报go mod不存在的问题,原因是go 1.16之后推荐使用go module构建项目,go module可以将某个项目(文件夹)下的所有依赖整理成一个 go.mod 文件,里面写入了依赖的版本等。所以需要执行一步:

1
>> go init mod <文件名>

Go语法:

  1. go语言之旅-中文