Golang 快速上手手册 | 基于C++/Python语言基础
前言
作为一名软件工程大二的学生,我有幸通过字节的青训营快速入门了Golang。基于我已有的C++和Python语言基础,我制作了这份Golang快速上手手册。如果你在阅读过程中发现任何错误或不足之处,欢迎随时指出。
必须知道的事
Go 虽然常被称为“21世纪的C语言”,但个人认为其语法风格更接近于Python或TypeScript。Go 以其简单易学、编译速度快以及适用于高性能服务而著称。对于熟悉Python的开发者来说,学习Go会相对容易。与Python不同,Go的突出特点是一个字——快。
Go 的特点
- 简单易学:Go 设计时减少了不必要的复杂性,使得开发者能够快速上手。
- 编译速度快:Go 的编译器非常高效,能够快速生成可执行文件。
- 高性能:Go 适用于开发需要高并发和高性能的服务。
Go 的应用场景
- 网络服务:Go 在处理高并发请求时表现出色,适用于Web服务开发。
- 命令行工具:Go 编译生成的二进制文件体积小,适合开发跨平台的命令行工具。
- 微服务:Go 的轻量级特性和强大的并发支持使其成为微服务架构的理想选择。。
希望这份手册能帮助你快速上手 Go 语言! 如果你还没有成功安装Go语言,请查阅此文档(如果觉得VSC配置有些繁琐,可以直接下载Goland进行无脑使用,并且Goland可以申请教育许可证免费使用)。同时附上Golang官方开发文档Documentation - The Go Programming Language。
Hello World!
每个语言的开始都是从Hello World!开始,现在我们同样以 Hello World!作为示例:
package main // 这是Go语言作为可执行代码的条件
import "fmt" // 输入输出库 可以视作C++的#include<iostream>
//main()函数是Go语言的入口函数,如同C/C++等语言一样。
func main() {
fmt.Println("Hello,World!") // 这里的Println()函数是来源于"fmt"库,而"fmt"库恰好在上文中调用
//不仅如此,还可以像C语言的printf()一样打印Hello World!
//s := "Hello world!" (这里的:=是赋值,详细在后文中有所体现)
//fmt.Printf("%s", s)
}
变量
Go有如下几种方式定义变量
package main
import "fmt"
func main() {
// 使用 var 类似JavaScript中的 let 赋值方法
var age int = 30
fmt.Println("Age:", age)
// var可以像这样单独声明变量类型
var a int
//以及像C/C++中结构体一样声明类型
var (
b bool
c float32
d string
)
// 使用一种类似Python中海象运算符的赋值方式(这一般用于for循环或者临时变量的赋值)
name := "John"
fmt.Println("Name:", name)
// 多重赋值,等号右边的数字将会依次赋值给左边依次的数字
var a, b, c int = 1, 2, 3
fmt.Println("a:", a, "b:", b, "c:", c)
}
类型
Go 提供了一系列丰富的类型,包括数值类型、布尔类型、字符串类型、错误类型,以及创建自定义类型(例如用于高精度运算的类型)。字符串是由 UTF-8 字符组成的序列,用双引号括起来。数值类型是最具灵活性的,既有带符号(
int)和无符号(uint)的 8、16、32 和 64 位整数。
byte是uint8的别名,rune是int32的别名。浮动类型(或浮点数)可以是float32或float64。也可以使用复数,可以表示为complex128或complex64。当声明一个变量时,它会被赋予对应类型的“空”值。例如,在
var k int中,k的值为 0;在var s string中,s的值为""(空字符串)。下面的示例展示了用户指定类型和使用短变量声明时默认类型之间的区别。
package main
import "fmt"
func main() {
//自定义类型
const a int32 = 12 // 32位整型
const b float32 = 20.5 // 32位浮点数
var c complex128 = 1 + 4i // 128位复数
var d uint16 = 14 // 16位无符号整型
var otherName rune = 1 // rune 是 int32 的别名
//默认类型
n := 42 // int (这里的int是几位取决于系统底层)
pi := 3.14 // float64
x, y := true, false // bool
z := "Go means CS:GO?" // string
fmt.Printf("自定义类型:\n %T %T %T %T %T\n", a, b, c, d, otherName)
// int32 float32 complex128 uint16 int32
fmt.Printf("默认类型:\n %T %T %T %T %T\n", n, pi, x, y, z)
// int float64 bool bool string
//`fmt.Printf()` 参数中的 `%T` 表示打印类型。在 Go 中,它表示传入变量的类型(类似C语言的printf()一样)。
}
数组
在 Go 中,存储多个元素可以通过数组、切片和映射(map)来实现,这里将一一解释实现方式。其中,数组的大小是固定的,并且所有元素都具有相同的数据类型。有趣的是,数组的大小是类型的一部分,这意味着数组的大小不能改变,否则它就会是不同的类型。数组元素通过方括号访问(如同C语言自带的数组一样)。下面的示例展示了如何声明一个包含字符串的数组以及如何遍历字符串数组。
package main
import "fmt"
func main() {
//这里定义了一个长度为4的字符串数组
var strings = [4]string{"Golang", "just", "like", "Python"}
//这里使用for 循环对string数组进行遍历
for i := 0; i < len(strings); i++ {
option := strings[i]
fmt.Println(i, option)
}
}
这将获得如下输出:
切片
切片可以被认为是动态数组。只因为切片本质上就是可以变动大小的数组。通过切片可见的元素数量决定了其长度。类似字符串或者c++中vector类型,也类似于Java、Python、JavaScript以及Ruby等现代语言中的基本数组(取切片都是前闭后开即s:=a[i:j] 将会包含从a[i]到a[j-1]而不是a[j])。下面是一个例子:
package main
import "fmt"
func main() {
/* 定义一个包含编程语言的数组 */
languages := [9]string{
"C", "Lisp", "C++", "Java", "Python",
"JavaScript", "Ruby", "Go", "Rust", // 必须包含尾部逗号
}
/* 定义切片 */
classics := languages[0:3] // 这里的0可以缺省,即[:3] ,缺省表示0
// 同理 [7:]也可以表示[7:9]缺省表示最后
modern := make([]string, 4) // len(modern) = 4
modern = languages[3:7] // 前闭后开 包含3不包含7
new := languages[7:9]
fmt.Printf("经典语言: %v\n", classics)
fmt.Printf("现代语言: %v\n", modern)
fmt.Printf("最新式语言: %v\n", new)
}
这将获得以下输出:
再看一个例子
package main
import (
"fmt"
"reflect"
)
func main() {
allLangs := languages[:]
fmt.Println(reflect.TypeOf(allLangs).Kind()) // 类似type()函数用于取类型
// frameworks 是一个切片包含一系列的Web框架
frameworks := []string{
"React", "Vue", "Angular", "Svelte",
"Laravel", "Django", "Flask", "Fiber",
}
jsFrameworks := frameworks[0:4:4] // 从0开始到3,步长为4
frameworks = append(frameworks, "Meteor") // 将"Meteor"加入frameworks切片
fmt.Printf("all frameworks: %v\n", frameworks)
fmt.Printf("js frameworks: %v\n", jsFrameworks)
}
这将获得以下输出:
映射 Map
如同Python的字典(dic)以及C++中的STL库 map;表示一系列键值对例如{“age”:10}
package main
import "fmt"
func main() {
//这里的map类型为string:int
firstReleases := map[string]int{
"C": 1972, "C++": 1985, "Java": 1996,
"Python": 1991, "JavaScript": 1996, "Go": 2012,
}
//通过range遍历map并输出(输出顺序是无序的,同样的代码输出会有不同的结果,随机的)
for k, v := range firstReleases {
fmt.Printf("%s was first released in %d\n", k, v)
}
}
得到如下输出:
控制流
if-else/switch/for用法
package main
import "fmt"
func main() {
number := 10
if number > 0 {
fmt.Println("数字是正数")
} else if number < 0 {
fmt.Println("数字是负数")
} else {
fmt.Println("数字是零")
}
// 简短声明
if num := 5; num > 0 {
fmt.Println("简短声明的数字是正数")
}
grade := "B"
/*=========================================================================*/
switch grade {
case "A":
fmt.Println("优秀")
case "B":
fmt.Println("良好")
case "C":
fmt.Println("中等")
case "D":
fmt.Println("及格")
case "F":
fmt.Println("不及格")
default:
fmt.Println("无效成绩")
}
// switch后可以不跟变量
score := 85
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 70:
fmt.Println("中等")
default:
fmt.Println("及格以下")
}
/*=========================================================================*/
// 1. 经典 for 循环
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// 2. while 循环 (for + 条件) Go没有while用法,用的是for的形式
j := 0
for j < 3 {
fmt.Println(j)
j++
}
// 3. 无限循环 (for)
// for {
// fmt.Println("无限循环")
// }
// 4. 使用 range 遍历 slice
numbers := []int{10, 20, 30}
for index, value := range numbers {
fmt.Println("Index:", index, "Value:", value)
}
// 5. 使用range 遍历 map
myMap := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for key, value := range myMap {
fmt.Println("key:", key, "value:", value)
}
}
这将得到如下输出:
结构体和指针
指针跟C++的几乎一样,但是Go语言不允许进行指针运算从而避免了不必要的安全问题;
- 指针存储的是变量的内存地址。
- & 是取地址符,用来获取变量的内存地址。
- * 是解引用符,用来通过指针访问指针指向的内存地址的值。
- 指针的零值是 nil,表示未指向任何内存地址。
- 通过指针,函数可以直接修改外部的变量值。 (这是非常重要的,在 Go 语言中,函数参数默认是值传递,使用指针可以传递地址,实现引用传递的效果。)
package main
import "fmt"
func main() {
// 1. 定义变量和指针
var num int = 10 // 定义一个整型变量
var ptr *int // 定义一个指向整型的指针变量
// 2. 获取变量的地址
ptr = &num // & 是取地址符, ptr 存储了 num 的内存地址
fmt.Println("num 的值:", num) // 输出: 10
fmt.Println("num 的地址:", &num) // 输出: 0xc0000b2008 (内存地址,每次运行可能不同)
fmt.Println("ptr 的值 (存储的地址):", ptr) // 输出: 0xc0000b2008 (和 &num 相同)
fmt.Println("ptr 的地址:", &ptr) // 输出 ptr 变量自己的地址
fmt.Println("ptr 指向的地址的内容:", *ptr) // 输出 10, * 是解引用符
// 3. 通过指针修改变量的值
*ptr = 20 // * 是解引用符,通过 ptr 指针访问 num 的地址,并修改值
fmt.Println("修改后 num 的值:", num) // 输出: 20
// 4. 指针的零值
var ptr2 *int
fmt.Println("未初始化的指针的值:", ptr2) // 输出: <nil> (指针的零值是 nil)
// 5. 使用指针作为函数参数
modifyValue(&num) // 传递 num 的地址到函数中
fmt.Println("函数修改后 num 的值:", num) // 输出: 30
}
// 6. 使用指针修改函数外部的变量值
func modifyValue(p *int) {
*p = *p + 10 // 通过指针 p 修改外部传入的变量的值
}
- 跟C++依然一样:结构体是一种自定义的数据类型并使用.运算符访问结构体成员。
package main
import "fmt"
// 1. 定义一个简单的结构体
type Person struct {
Name string
Age int
City string
}
// 2. 定义嵌套结构体
type Address struct {
Street string
ZipCode string
}
type Employee struct {
Person // 匿名结构体
Id int
Address Address // 嵌套结构体
}
// 3. 定义带有方法的结构体
type Circle struct {
Radius float64
}
// 定义 Circle 的方法 (计算面积)
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
// 定义 Circle 的方法 (修改半径) (注意使用指针接收者)
func (c *Circle) SetRadius(r float64) {
c.Radius = r
}
func main() {
// 4. 创建结构体实例
// a. 使用字面量
person1 := Person{
Name: "张三",
Age: 30,
City: "北京",
}
// b. 按顺序
person2 := Person{"李四", 25, "上海"}
// c. 不指定值的字段,会使用该字段的零值
person3 := Person{Name: "王五"}
fmt.Println("person1:", person1)
fmt.Println("person2:", person2)
fmt.Println("person3:", person3)
// 5. 访问结构体成员
fmt.Println("person1 的名字:", person1.Name)
fmt.Println("person1 的年龄:", person1.Age)
fmt.Println("person1 的城市:", person1.City)
// 6. 创建嵌套结构体实例
employee1 := Employee{
Person: Person{Name: "广州人", Age: 35, City: "广州"},
Id: 123,
Address: Address{Street: "珠江新城", ZipCode: "100000"},
}
fmt.Println("employee1:", employee1)
fmt.Println("employee1 的姓名:", employee1.Name) // 匿名结构体可以直接访问内嵌结构的字段
fmt.Println("employee1 的地址:", employee1.Address)
fmt.Println("employee1 的地址的街道:", employee1.Address.Street)
// 7. 创建结构体指针
person4 := &Person{Name: "蔡徐坤", Age: 40, City: "深圳"}
fmt.Println("person4 的名字:", person4.Name) // 指针访问成员可以使用 person4.Name
fmt.Println("person4 的地址:", person4)
// 8. 使用带有方法的结构体
circle1 := Circle{Radius: 5.0}
fmt.Println("circle1 的面积:", circle1.Area()) // 调用 Area 方法
circle1.SetRadius(10) // 调用 SetRadius 方法修改半径
fmt.Println("circle1 修改半径后的面积:", circle1.Area())
fmt.Println("circle1 的半径:", circle1.Radius)
}
运行结果如下:
并发
下图解释了为什么说并发是Go的优势:
Go 语言的并发模型更注重轻量级、高效的并发,其基于通信的并发模型和内建的并发原语使得并发编程更加简单、安全和高效。
package main
import (
"fmt"
)
func main() {
c := make(chan int) // 创建一个用于传递整数的通道 (channel)
for i := 0; i < 5; i++ {
go cookingGopher(i, c) // 启动 5 个 goroutine 执行 cookingGopher 函数
}
for i := 0; i < 5; i++ {
gopherID := <-c // 从通道接收一个整数,表示一个 gopher 完成了烹饪
fmt.Println("gopher", gopherID, "finished the dish") // 打印完成烹饪的 gopher 的 ID
}
// 所有 goroutine 在这里都已完成
}
/* 注意:通道作为参数传递 */
func cookingGopher(id int, c chan int) {
fmt.Println("gopher", id, "started cooking")
c <- id // 将 gopher 的 ID 发送到通道,通知主 goroutine 它已完成
}
如上代码模拟了五个厨师进行烹饪比赛,用于快速理解并发,并将获得以下输出:
恭喜你 ,你已经完成Go语言基础语法的速成,现在写一个素数筛作为速成成果吧!
package main
import "fmt"
func EulerSieve(n int) []int {
isPrime := make([]bool, n+1)
primes := make([]int, 0)
for i := 2; i <= n; i++ {
isPrime[i] = true
}
for i := 2; i <= n; i++ {
if isPrime[i] {
primes = append(primes, i)
}
for _, p := range primes {
// 下划线_可以用来接受一些不想要的函数的返回值,一般用于占位控制传参顺序
if i*p > n {
break
}
isPrime[i*p] = false
if i%p == 0 {
break
}
}
}
return primes
}
func main() {
n := 100
primes := EulerSieve(n)
fmt.Printf("Prime numbers up to %d:\n", n)
fmt.Println(primes)
}