入门

Go语言入门

入门

一个简单的hello world程序

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello world")
}

package关键字用于声明这个文件源文件属于哪个包, 一个文件夹下面直接包含的文件只能归属一个包,同一个包的文件不能在多个文件夹下. 文件夹的名称可以与包名不同

import关键字, 用法import [importname] "/path/to/package", 用于导入包, importname可以省略, 省略后, 默认值为引入包的包名.

go语言依赖包查找机制

  • Go 支持 Go Modules 之后,编译时编译器会从工作目录(当前所在目录)开始并逐级向上查找是否具有 go.mod 文件。
    • 如果有,go.mod 文件中声明的 module 名称就视作 go.mod 所在的路径,然后以指定的 main 包为依赖入口,所有以 go.mod 中声明的 module 名称开头的导入路径都以 go.mod 所在的路径为相对路径进行包的查找导入。所有需要导入的路径中如果在 go.mod 中指定了版本,则从 GOMODCACHE 下取得相应版本进行导入,如果没有被指定则从 $GOPATH/src/$GOROOT/src/ 中进行查找导入。GOMODCACHE, 它默认为 $GOPATH/pkg/mod
    • 如果没有,所有依赖均从 $GOPATH/src/$GOROOT/src/ 中进行查找导入。
  • 编译时,不在乎源文件在哪(只要指定入口,依赖路径便可依次拿到),而是在乎工作目录在哪(从工作目录开始逐级向上查找,是否可以找到 go.mod

参考链接: https://www.obooks.net/post-671.html

go环境变量

1
2
3
4
5
6
7
8
9
# 查看环境变量
go env

# 设置环境变量
# Go version >= 1.13 当你的GO的版本大于1.13的时候. 低版本的, 几乎用不到, 不用管了
go env -w GOPATH=D:\xiaojian\GoLand

# 取消环境变量
go env -u GOMODCACHE

Go的命令

1
2
3
4
5
6
7
8
# 编译, 并运行, 运行完成后, 可执行文件会被删除
go run helloworld.go

# 编译成可运行的二进制文件, 会放在当前目录下, 只对main包有效, 对于非main包, 没有出错, 也没有效果
go build helloworld.go

# 下载依赖
go get 包地址

Go语言的命令可能和网上的说明差异很大, 准确的描述, 只能看go help

命令行参数

os.Args是一个slice, 其中os.Args[0]go程序本身的名字

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

import (
"fmt"
"os"
)

func main() {
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}

go语言中变量的声明有4种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 由于string可以省略, 很少使用这种方式
var s string = "caicai"
var s = "caicai"
# java局部中变量不会有初始化, 如果直接使用, 会报没有初始化, 成员变量和静态变量才会初始化, 默认为零值, go语言中, 任何地方都会初始化为相应类型的零值
var s string
# 只能用于局部变量的声明
s := "caicai"

# 多变量声明, 可读性不好, 不建议使用
# go很重的也发
var s, sep string
var s, sep = "caicai", " "
s, sep = "", ""
# java中的写法
String s, sep;
String s = "caicai", sep = " ";

go语言中++--是语句, 不是表达式, 不支持j = i++, 但在java中, ++--是语句, 支持j = i++;

go语言中循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# while循环
for condition {

}

# 无尽循环
for {

}

# 普通for循环
for initialization; condition; post {

}

# range循环, 有点像java的foreach循环, 但是go中可以有index
for index, value := range slice {

}

go中数组时支持打印的, java中数组如果不使用工具类, 打印的是hash

查找重复的行

gomap的键不能为不能比较的类型, 例如slice, 值可以是任何类型. java中键值只要是引用类型就行

go中打印结构体的办法

使用fmt.Printf

  • %v占位符是不会打印结构体字段名称的,字段之间以空格隔开;
  • %+v占位符会打印字段名称,字段之间也是以空格隔开;
  • %#v占位符则会打印结构体类型和字段名称,字段之间以逗号分隔

指针类型, 只会打印地址, 如果需要打印值, 就需要实现String方法, 跟java一样, 但这个方法只对%v%+v有效, 如果想%#v打印值, 需要实现GoString

使用json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"encoding/json"
"fmt"
)

type Teacher struct {
Name string
ID int
Age int
Address string
}

func main() {

s1 := Teacher{
Name: "Jason Yin",
ID: 001,
Age: 18,
Address: "北京",
}

marshal, _ := json.Marshal(&s1)
//marshal, _ := json.Marshal(s1)
fmt.Println(string(marshal))
}

go从标准输入和文件读入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 从标准流读入
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
s := input.Text()
}

// 从文件当中读入
f, err := os.Open("1.txt")
input := bufio.NewScanner(f)

// 写入文件, 方式1, 会创建文件
os.WriteFile("1.txt", []byte("xx"), 0666)
// 写入文件, 方式2, 不会创建文件
//f, err := os.Open("1.txt")
f, err := os.Create("1.txt")
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return
}
defer f.Close()
f.Write([]byte("xxx"))

// java从标准流读入
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
// java从文件读入
try (BufferedReader reader = new BufferedReader(new FileReader("hello.txt"))) {
String s = "";
while ((s = reader.readLine()) != null) {
System.out.println(s);
}
}
// 写入文件
BufferedWriter writer = new BufferedWriter(new FileWriter("1.txt"))
writer.write("xxx")

函数和包级别的变量可以任意顺序声明, 并不影响其调用. 这个和java一样

GIF动画

当我们import了一个包路径包含了多个单词的package时, 比如image/color, 通常我们只需要用最后那个单词表示这个包就可以.

常量是指在程序编译后运行时始终不会发生变化的值. 目前常量声明的值必须是一个数字值, 字符串或者一个固定的boolean值.

[]color.Color{...}gif.GIF{...}是复合声明的一种写法. 这里前者生成的是一个slice切片, 后者生成的是一个struct结构体.

struct是一组值或者叫字段的集合, 不同的类型集合在一个struct可以让我们以一个统一的单元进行处理, struct内部的变量可以以一个点.来进行访问.

并发获得多个URL

goroutine是一种函数的并发执行方式, 而channel是用来在goroutine之前进行参数传递.

main函数本身也运行在一个goroutine中, 而go function则表示创建一个新的goroutine, 并在新的goroutine中执行这个函数.

当一个goroutine尝试在一个channel上做send或者receive操作时, 这个goroutine会堵塞在调用处, 直到另一个goroutine往这个channel里写入, 或者接收值, 这样两个goroutine才会继续执行channel操作之后的逻辑.

Web服务

Go允许一个简单的语句(如一个局部变量声明)写在if条件的前面, 这在错误处理的时候特别有用, 这样可以缩小err变量的作用域.

本章要点

switch语句: Go语言并不需要显式地在每一个case后写break, 语言默认执行完case后的逻辑语句会自动退出. 如果需要贯穿, 需要自己显式地写上一个fallthrough语句来覆盖默认行为.

label语句: 允许breakcontinue跳过任意层的循环, 调到label处.

命名类型: 类型声明使得我们可以很方便地给一个特殊类型一个名字

1
2
3
4
5
type Point struct {
x, y int
}

var p Point

指针: Go语言提供了指针. 指针是一种直接存储了变量内存地址的数据类型. 指针是可见的内存地址, &操作符可以返回一个变量的内存地址, 并且*操作符可以获取指针指向的变量内容, 但是在Go语言中没有指针运算, 也就是不能像c语言里可以对指针进行加或减操作.

方法和接口: 方法是和命名类型关联的一类函数. 接口是一种抽象类型, 这种类型可以让我们以同样的方式来处理不同的固有类型, 不用关心它们的具体实现.

多行注释: /* ... */.

常用的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 可以打印以空格间隔的一个或多个值, 并在最后添加一个换行符, 从而输出一整行
fmt.Println()

// os.Args变量是一个字符串的切片. os包提供了一些函数和变量, 以与平台无关的方式和操作系统打交道. 命令行参数以os包中Args名字的变量提供程序访问, 在os包外面, 使用os.Args这个名字. os.Args的第一个元素是os.Args[0], 它是命令本身的名字; 另外的元素是程序开始执行时的参数
os.Args

// 该变量重程序的标准输入中读取内容. 每次调用input.Scan()函数, 即读入下一行, 并移除行末的换行符; 读取的内容可以调用input.Text()得到. Scan函数在读到一行时返回true, 在无输入时返回false
input := bufio.NewScanner(os.Stdin)
input.Scan()
input.Text()

// fmt.Printf函数对一些表达式产生格式化输出
fmt.Printf()

// %d 十进制整数
// %x, %o, %b 十六进制, 八进制, 二进制
// %f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
// %t 布尔: true或false
// %c 字符(rune)(Unicode码点)
// %s 字符串
// %q 带双引号的字符串"abc"或带单引号的字符'c'
// %v 变量的自然形式
// %T 变量的类型
// %% 字面上的百分号标志

// os.Open返回两个值. 第一个值是被打开的文件(*os.File), 其后被Scanner读取. os.Open返回第二个值是内置error类型的值.
os.Open()

// 向文件中写入字符串, 输入和输出流也是文件
fmt.Fprintf()

// 读取指定文件的全部内容, 返回一个字节切片, 必须使用string, 才能用strings.Split进行分割
ioutil.ReadFile()

// 把字符串切割成子串的切片
strings.Split()

// 把字符串切片以指定分隔符连在一起
strings.Join()

// 生成一个字符串
fmt.Sprintf(format string, a ...any) string

Panic异常

1
2
3
4
// 手动抛出异常
panic(fmt.Sprintf("invalid suit %q", s))

// go中推荐使用错误机制, java中因为编译时异常不方便扩展, 只能推荐使用运行时异常

Recover捕获异常

1
2
3
4
5
6
7
8
9
10
// go中捕获异常异常只能在defer语句中使用
// recover可以让使用该defer语句的函数从panic中恢复

func Parse() {
defer func() {
if p := recover; p != nil {
fmt.Printf("%v\n", p)
}
}()
}

方法

方法声明

1
2
3
4
5
6
7
8
9
10
11
12
13
// go语言中方法和结构体绑定的方式是在方法名称声明前写明类型, 在java中类有作用域, 在类的括号中, 就是类的方法
// go语言中指明当前对象, 用的是接收器变量, java中是用this
// go语言中方法是一类公民, 和变量一样, 属性和方法同名是会出现编译问题的, 在java中, 方法和属性完全不同, 因此可以同名

// go语言中经常会有这种写法, 在java中, path会使用一个List成员属性来包含Point, 而不是继承至List
// go语言中这种写法, 有一个好处, 可以直接使用数值, 字符串, slice, map的一些特性, 例如"+", 遍历啥的
type Path []Point
type A map[string][]string

// go语言中, 方法可以被声明到任意类型, 只要不是一个指针或一个interface
// 下面的例子说明了了原因, 是为了避免歧义, 这时不知道时给哪个类型定义了方法, 是P, 还是int呢
type P *int
func (p P) f() { /* ... */ } // compile error: invalid receiver type

基于指针对象的方法

1
2
3
4
5
// go语言中如果方法想改变结构体的值, 声明的接收器就必须是结构体的指针
// go语言中如果接收器p是一个非指针的变量, 并且其方法需要一个指针类型作为接收器, 可以不手动取地址, 与之对应, 针对调调用者本身是指针类型, 需要调用非指针的类型的方式时, go编译器也会自动进行解引用. 这两个操作, 只会对方法有效, 不会对属性有效
// 这就能解释数组指针, 不进行解引用, 就能直接进行range和[]操作
// go语言中结构体不建议, 既有值的方法, 也有指针的方法
// 针对接口, 结构体实现的值方法, 会自动生成指针方法, 但指针方法不会生成值方法

Nil也是一个合法的接收器类型

1
2
3
4
5
6
7
8
9
10
// go语言中nil可以用于调用方法, 返回什么, 由方法决定, 在java和很多其他语言中, 都会报空指针异常. go中这样弄, 抛空指针的panic就由方法实现者来实现了
// go语言中, nil的slice可以直接用append添加值, 但对于nil的map用[]添加值时, 会出错

// 很奇怪的初始化, 没有指明类型, 只有{}, 感觉像自动转型了一样
type A map[string][]string
a := A{"1": {"1"}}
fmt.Println(a)
type B []string
b := B{"1"}
fmt.Println(b)

通过嵌入结构体来扩展类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// go语言中内嵌结构的方法, 是通过委托的方式, 让外部结构体也有这些方法的
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}

// 嵌入式结构体, 让匿名结构也可以拥有一些方法了
// 例如cache的结构就没有名称, 就没有办法给这个结构体定义方法, 但是由于有sync.Mutex这个结构体, 就有了Lock()和UnLock方法
var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
}

方法值和表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// go语言中new函数的用法, 返回对应类型的指针
type Rocket struct{}
func (r *Rocket) Launch()
r := new(Rocket)
time.AfterFunc(10 + time.Second, func(){r.Launch()})
// 可以直接用
time.AfterFunc(10 * time.Second, r.Launch)
// 这说明如果方法已经绑定了对象, 这时就称为了方法值, 和函数值一样了

// java中没有方法值, 函数值这种东西
// 方法值除了可以指定对象, 还可以用指定了类型的方式, 这时必须写正确是类型还是类型指针
distance := Point.Distance // method expression
scale := (*Point).ScaleBy
scale(&p, 2)

Bit数组

1
// go语言中对于字符串拼接, 也有高效的类, bytes.Buffer, 相当于java中StringBuilder

类型开关

在go语言中, 接口有两种使用方式

  1. java中的接口那样, 用于隐藏属性, 重点在于方法, 表示行为
  2. java中的Object那样, 不是为了隐藏属性, 而是为了暴露, 这种方式接口几乎没有任何方法, 操作它们的函函数通常会使用类型开关的, 这种行为可以被称为可辨识联合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// go中的类型开关, go中swith好像与顺序有关
var x any
switch x.(type){
case nil:
case int, uint:
case bool:
case string:
default:
}

func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x)
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x)
default:
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}

Channels

go语言中, make可以用于创建slice, map, channel

1
2
3
4
5
6
7
8
9
ch := make(chan int)
ch := make(chan int, 3)
ch <- x
x <-ch
<- ch
// 关闭channel, 往一个已经关闭channel中发数据会报panic, 但还是可以读关之前的数据, 如果channel中已经没有数据了, 将会返回零值
close(channel)

// channel有点像java中消息队列

channel是引用类型, 两个相同类型的channel可以用==比较. 如果两个channel引用的是相通的对象, 那么比较的结果为真

不带缓存的Channels

不带缓存的Channel对导致堵塞发送者和接收者goroutine

go语言中没有volatile, 但可以用channel的收发来保证执行顺序和可见性

基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作. 当通过一个无缓存Channels发送数据时, 接收者受到数据发生在唤醒发送者goroutine之前

串联的Channels(Pipeline)

go语言中channel不是资源, 不是一定要像文件那样, 必须关闭的. go会自动回收不用的channel, 其实只有当需要告诉接收者发送完毕了, 才需要关闭channel.

同时重复关闭channel会报panic, 关闭一个nil值的channel也会导致panic异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Squarer
go func() {
for {
x, ok := <-naturals
if !ok {
break // channel was closed and drained
}
squares <- x * x
}
close(squares)
}()

// 检查channel是否关闭的另外一种语法
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()

反射

为什么需要反射

注意事项

  1. 空silce和值为nil的slice是等效的, 但map不是这样
-------------本文结束感谢您的阅读-------------