Golang 开发基础

经过近九年的校园学习和职业发展,我从最初的 C 语言硬件开发,逐渐转向 Java 互联网开发。在这个过程中,我深刻体会到了 Java 开发的便捷之处,它让我摆脱了繁琐的结构体定义和手动内存地址操作的束缚。然而,Java 的缺点也很明显,那就是 JVM 庞大的内存占用,一个 2G RAM VPS 基本上只能跑一到两个 Java 服务。

幸运的是,我发现了 Go 语言,它不仅具备可与 Java 媲美的开发便捷性,同时在运行时内存占用极低,更令人惊喜的是它还支持自动垃圾回收,简直是一个完美的选择!因此,我决定花一些时间深入学习 Go 语言,以期在未来的项目中更灵活地运用这一强大的编程语言。

1 Golang 简介

Golang 是由 Google 开源的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。

Golang 的语法整体上与 C 语言语系相近,但在变量声明方面有一些不同。相较于 C++,Go 摒弃了枚举、异常处理、继承、断言、虚函数等功能,但增加了切片、并发、管道、垃圾回收、接口等特性的语言级支持

2 环境安装与配置

2.1 运行环境安装

Go 环境的安装相当简单,去官网的 下载地址 下载对应平台的安装程序即可:

下载 golang
  • 这里我选择下载最新版本 (1.21.6) 的 amd64 msi 文件进行安装。另外,我们还能下载 go 的源码 (Source) 进行阅读。

2.2 第一个 Go 程序

安装完成后,我们创建一个 main/helloworld.go 文件,编写一个最基础的 Hello World 代码:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

然后在控制台执行 go run helloworld.go 命令:

1
Hello, World!

控制台成功打印出了结果。

2.3 IDE 软件安装

在进行实际项目开发时,显然不能使用控制台这种刀耕火种的模式,因此我们需要选择一个适用的 IDE,这里我推荐 Jetbrains 家的 GoLand

goland

有了集成开发环境,后续编写大型工程就更加方便了。

3 Golang 语言基础

3.1 语言结构

Golang 程序的核心分为以下几个部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main // 包的声明

import "fmt" // 包的导入

func Hello(name string) string { // main 函数
	var format = "Hi, %v. Welcome!"
	message := fmt.Sprintf(format, name)
	return message
}

func main() {
	hello := Hello("maling")
	fmt.Println(hello)
}
  • :包的声明,也就是 package 关键字。
    • Golang 以“包”为单位进行依赖包的组织;
    • 一般主函数会以 package main 命名,其他的包则与目录名保持一致。
  • 导入:包的导入,也就是 import 关键字;
    • 在编写了包后,我们会使用 import xxx 进行包的方法导入;
    • 包分为三种:标准库、第三方库、本地库;
    • 若是 import \_ xxx 的方式,则代表则导入该包的初始化方法。
  • 主函数:应用程序入口。
    • 整个应用程序仅此只有一个入口;
    • 主函数必须要有包声明,且必须以 package main 命名 。
  • 普通函数:声明一个普通函数:
    • ...(name string):表示字符串参数,参数类型为 string,参数名称为 name
    • ...) string {...:表示返回值类型为 string
    • var format = "Hi, %v. Welcome!":表示声明一个 format 变量,并将其内容赋值为字符串 "Hi, %v. Welcome!"
    • hello :=:表示接收 Hello() 函数的返回值。

3.2 基础语法

作为经验丰富的开发者,学习一门新的开发语言再简单不过了,只需关注与我们主要使用的开发语言的区别即可。

3.2.1 分隔符

Golang 通过换行来分隔不同语句,而不需要像 Java 一样显式的使用 ; 来分隔语句。

3.2.2 代码注释

Golang 的注释与 Java 一样,可以通过 // 注释行,/**/ 注释块。

3.2.3 变量名称

Golang 的变量名称可以包含字母、数字和下划线 _,而且变量名的起始字符必须是字母或下划线 _,不能是数字。

相比于 Java 语言,Golang 的变量名称不支持 $ 符号,这点需要注意。此外,Golang 变量名也不能与 funcvar 等关键字冲突。

3.2.4 关键字

Golang 包含以下关键字:

关键字含义
package用于定义代码所属的包
import导入包
func用于定义函数
interface用于声明接口类型
关键字含义
for用于循环执行代码块
continue循环控制
switch用于多路分支选择
select用于 I/O 多路复用
case在 switch 语句中,用于定义不同的匹配情况
default在 switch 语句中,表示没有任何一个 case 匹配时执行的代码块
fallthrough在 switch 语句中,表示穿透到下一个 case
break用于终止 for、switch 或 select 的循环
if、else用于条件判断
关键字含义
go用于启动一个新的goroutine(基于协程实现)
map用于声明映射集合类型
struct声明结构体
chan用于声明通道(goroutine 之间传递数据)
goto用于无条件跳转到指定的代码标签
var声明变量
const声明常量
range用于迭代数组、切片、映射或通道
type用于定义新的数据类型
defer用于在函数执行完毕之前延迟执行一些代码
return结束控制

此外,Go 语言还包括 36 个预定义标识符,其中大部分表示变量类型:

关键字含义
make用于创建切片、映射或通道
new用于分配新的内存地址
append用于向切片追加元素
cap返回切片的容量
close用于关闭通道
copy用于复制切片元素到目标切片
nil空值,通常用于指针、切片、映射、通道或函数等
iota枚举常量
print用于向标准输出打印信息
println添加换行符的 print
len返回字符串、切片或数组的长度
recover用于从 panic 中恢复
true、false两个布尔值
关键字含义
int,int8,int16,int32,int64整数类型
float32,float64浮点数类型
uint,uint8,uint16,uint32,uint64无符号整数类型
complex,complex64,complex128复数类型
real,imag复数的实部与虚部
uintptr指针的整数类型
bool,byte,string布尔、字节、字符串类型

3.3 数据类型

上表已经列出了 Golang 支持的数据类型。Golang 结合了 C 与 Java 的优点,原生支持了数据类型的比特位,通过原生支持数据类型的比特位,使得开发者能够精确地控制内存占用。这一特性不仅实现了接近 C 语言的底层内存控制能力,同时也融合了 Java 语言的安全性和开发者友好性。

与 Java 相比,Golang 也支持基础的布尔型和数字类型(整型、浮点型)。然而,Golang 在原生层面直接支持字符串类型 string,而不像 Java 还需要通过 String Class 进行封装。

3.4 语言变量

Golang 通过以下方式声明变量:

1
var v_name t_type
  • var:用于声明变量;
  • v_name:表示变量名称;
  • t_type:表示变量类型;

当声明变量后,会被赋予一个默认值,如果是数据类型会被赋予 0 值,如果是地址类型则会被赋予空值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var i int
var f float64
var b bool
var s string
var _i *int
var _ia []int
var __m map[string]int
var _c chan int
var _f func(string) int
var e error

fmt.Println(i, f, b, s, _i, _ia, __m, _c, _f, e)

结果总结如下:

数据类型默认值
int0
float640
boolfalse
string""
*int<nil>
[]int[]
map[string]intmap[]
chan int<nil>
func(string) int<nil>
error<nil>

3.5 语言常量

3.5.1 const

Golang 支持与 C 语言一样的 const 关键字:

1
const c_name [c_type] = c_value
  • const: 用于声明常量;
  • c_name:表示常量名称;
  • [c_type]:表示常量类型(可缺省,编译器会根据 c_value 推测);
  • c_value:常量的值,一旦设定无法更改。

在 Java 中,虽然没有 const 这个关键字,但是可以使用 final 来声明不可变量。他们的区别在于 final 类型引用指向的对象不可变,但是引用本身是可以再指向其他对象的。而 Golang 中的 const 常量完全不可变。

3.5.2 iota

除了 const,Golang 还支持一种特殊的自增常量关键字 iota

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const (
    a = iota   // 0
    b          // 1
    c          // 2
    d = "ha"   // 独立值 ha
    e          //
    f = 100    // 独立值 100
    g          //
    h = iota   // 恢复计数 7
    i          // 8
)

fmt.Println(a,b,c,d,e,f,g,h,i)

运行结果如下:

1
0 1 2 ha ha 100 100 7 8

const() 中可以通过 iota 返回自增的常量。就算被打断了,iota 也会继续计数。

3.6 运算符

与 C、C++ 一样,Golang 也支持以下运算符:

  • 算数运算符:+、-、*、/、%、++、–
  • 关系运算符:==、!=、>、<、>=、<=
  • 逻辑运算符:&&、||、!
  • 位运算符:&、|、^、«、»
  • 赋值运算符:=、+=、-=、*=、/=、%=、«=、»=、&=、|=、^=
  • 取地址符:&
  • 指针符号:*p

3.7 条件语句

Golang 与 C、C++ 一样,也支持 ifswitch 条件控制语句,此外 Golang 还提供了一个 select 机制。使用示例如下:

3.7.1 if

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// if 条件控制
if a == 1 {
    ...
} else {
    ...
    // if 嵌套
    if b == 2 {
        ...
    } else {
        ...
    }
}

3.7.2 switch

1
2
3
4
5
6
7
8
switch var1 {
    case val1:
        ... // do something
    case val2:
        ... // do something
    default:
        ... // do something
}

3.7.3 select

select 用于 goroutine 之间数据的通信,其中每个 case 必须是一个通信操作,通常是针对通道的读取或写入:

1
2
3
4
5
6
7
8
select {
  case <- channel1:
    ... // do something
  case <- channel2:
    ... // do something
  default:
    ... // do something
}

select 会等待多个通信操作中的任意一个就绪,然后执行相应的 case 语句。如果没有任何一个通信操作就绪,此时如果存在 default 语句,则执行该代码块;否则,直接阻塞等待。

3.8 循环语句

Golang 与 C 语言一样也支持 for 循环,且具备与 C 语言一样活跃的 goto 跳转能力(相比之下,Java 的只能通过 break label 的形式重启 for 循环):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {

	for true {
		fmt.Printf("loop")
		a++
		if ... {
			continue // 继续循环
		} else if ... {
			goto end // 跳转到标签
		} else {
			break // 跳出循环
		}
	}

end:
	return
}

此外,Golang 没有提供像 C、Java 那样的 while 循环

4 Golang 函数

4.1 函数的返回值

在前文中,我们已经学习了 Golang 语言的基本函数结构。然而,Golang 函数的强大之处不仅限于此,它还具备灵活返回多种不同类型值的能力,例如:

1
2
3
4
5
6
7
8
func swap(x string, y int) (string, int) {
   return x, y
}

func main() {
   a, b := swap("Google", 100)
   fmt.Println(a, b)
}

如果想在 Java 中实现类似的效果,还得使用 Pair<String, Integer> 这样的工具类才能完成。

4.2 函数的参数传递

我们知道,编程语言的参数传递往往有两种方式:

  • 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  • 引用传递:指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

4.3 函数变量

Golang 支持将函数声明为函数变量

1
2
3
getSquareRoot := func(x float64) float64 {
    return math.Sqrt(x)
}

被声明的函数变量可以像普通参数一样使用或传递:

1
fmt.Println(getSquareRoot(81))

4.4 匿名函数

Golang 支持与 C++ lambda 类似的匿名函数,语法如下:

1
2
3
4
5
// 匿名函数赋值给变量
add := func(x, y int) int {
    return x + y
}
fmt.Println(add(1, 2))

此外,匿名函数还可以直接调用:

1
2
3
4
5
fmt.Println(
    func(x, y int) int {
        return x - y
    }(10, 3)
)

5 Golang 的包

与 Java 类似,包 (package) 是组织和管理 Golang 代码的基本单元,每个 Go 源代码文件都必须属于一个包。包提供了代码的命名空间、可复用性、以及对代码的封装。以下是关于 Golang 包的一些基本概念:

  • 声明包:例如 package main,包名必须与目录名相同;
  • 导入包:用于到入其他包,例如 import "fmt"
  • 包的可见性:Go 语言中,标识符的可见性由其首字母的大小写决定;
    • 首字母大写的标识符在包外部可见,首字母小写的标识符仅在包内部可见
      1
      2
      3
      4
      5
      6
      7
      8
      
      // 首字母大写的函数,在包外部可见
      func PublicFunction() {
          fmt.Println("This is a public function.")
      }
      // 首字母小写的函数,对外不可见
      func privateFunction() {
          fmt.Println("This is a private function.")
      }
  • 包的组织: 一个包可以包含多个源文件,在同一目录下的源文件,只需要在其中一个文件中声明 package,其他文件会自动属于同一个包。
  • 包的初始化: Go 中的每个包可以有一个 init() 函数,该函数在包被导入时自动执行,用于初始化一些包级别的变量或执行其他初始化操作。

6 实战小程序

在学习了解了 Golang 的基础内容之后,接下来让我们使用 Golang 编写一个小程序。该程序的主要功能是爬取本站的所有图片(不过示例中没有下载,而是直接丢弃了):

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package main

import (
	"encoding/xml"
	"fmt"
	"golang.org/x/net/html"
	"io"
	"log"
	"net/http"
	"time"
)

func main() {
    // 1. 通过本站 sitemap,拿到所有 URL
    resp, err := http.Get("https://maling.io/sitemap.xml")
    if err != nil {
        log.Println(err)
        return
    }
    var Host = "https://maling.io"
    // 2. 定义 sitmap 的 xml 结构体
    type Url struct {
        Loc        string  `xml:"loc"`
        Lastmod    string  `xml:"lastmod"`
        Changefreq string  `xml:"changefreq"`
        Priority   float32 `xml:"priority"`
    }
    var sitemap struct {
        UrlSet []Url `xml:"url"`
    }
    // 3. 读取 XML 内容
    xmlBody, err := io.ReadAll(resp.Body)
    err = xml.Unmarshal(xmlBody, &sitemap)
    if err != nil {
        // I/O 异常则返回
        log.Println(err)
        return
    }
    _ = resp.Body.Close()
    urlSet := sitemap.UrlSet
    // 4. 便利 sitemap 中所有的 url,并忽略异常
    for _, url := range urlSet {
        time.Sleep(10 * time.Second)
        loc := url.Loc // loc 中存储的是 url
        fmt.Println(loc)
        // 5. 通过 http 获取页面的 body
        page, err := http.Get(loc)
        if err != nil {
            // I/O 异常则返回
            log.Println(err)
            return
        }
        var imgSlice []string
        // 6. 将 page 解析为 DOM
        doc, err := html.Parse(page.Body)
        if err != nil {
            log.Println(err)
            return
        }
        // 7. 递归解析 DOM 中所有的 img 标签,存入 imgSlice
        var f func(*html.Node)
        f = func(n *html.Node) {
            if n.Type == html.ElementNode && n.Data == "img" {
                attr := n.Attr
                for _, attribute := range attr {
                    if attribute.Key == "src" {
                        imgUri := attribute.Val
                        if imgUri[:1] == "/" {
                            imgSlice = append(imgSlice, Host+imgUri)
                        }
                    }
                }
            }
            for c := n.FirstChild; c != nil; c = c.NextSibling {
                f(c)
            }
        }
        f(doc)
        // 8. 下载 img url 指向的图片
        if len(imgSlice) > 0 {
            time.Sleep(2 * time.Second)
            for _, img := range imgSlice {
                response, err := http.Get(img)
                if err != nil {
                    log.Println(err)
                    continue
                }
                _, _ = io.ReadAll(response.Body)
                _ = response.Body.Close()
            }
        }
        _ = page.Body.Close()
    }
}

参考资料: