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

147 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第1天。
Go语言作为一门新兴的语言,以其高性能、语法简单、编译速度快等优势得到了越来越多企业和开发者的青睐.《Trends and Insights from GitHub 2022》这份报告显示,在2022年,Golang已经超过Ruby成为后端第三热门的语言,仅 次于Python与java。同时,它也是字节后端开发的主要语言。
让我们开启Go语言之旅吧,Let's GO!

1.特点与优势

  • 高性能、高并发,具有优秀的标准库
  • 语法简单,上手快
  • 丰富的工具链
  • 所有编译结构默认静态链接,在线上容器环境部署只需要拷贝一个可执行文件,部署非常方便快捷
  • 编译速度非常快
  • 跨平台,GO语言可以在市面上大部分操作系统上运行
  • 强大的垃圾回收功能

2.GO的包管理方案

GOPATH模式

GOPATH模式的项目结构如下:

  • GOPATH:相当于workspace,所有的GO开发都将在GOPATH下进行
  • bin:存放编译后的可执行文件
  • pkg:存放编译过程中产生的库文件
  • src:存放项目代码的位置

在GOPATH模式下,代码需要固定存放在$GOPATH/src目录下。使用go get下载外部依赖时,会默认安装到$GOPATH目录下。也就是说,在这种模式下,所有项目的代码必须存放在指定的GOPATH中。
除了必须指定目录,还有如下缺点:

  1. go get无法指定版本
  2. 引入第三方项目时,无法处理同项目不同版本的引用问题,因为在这种模式下,项目路径都是一样的,比如src/projectA.
  3. 无法同步第三方库的版本号。

GO MODULES

在GO1.11版本中,GO官方推出了新的包管理方案GO MODULES,简称GO MOD.
从go mod开始,GO的项目创建可以不再依赖GOPATH。此后,导入的第三方包将存放在GOPATH下的pkg文件夹中。
GOLAND中,新建项目时选择GO而不是GO(GOPATH)即可在GO MOD模式下创建项目。
需要使用 go env命令开启go mod模式:

go env -w GO111MODULE="on"

GOPROXY

由于众所周知的原因,当我们需要下载github上的某些包时,直连是很难下载下来的,因此我们需要配置代理,也就是GOPROXY环境变量。
使用以下命令配置代理:

go env -w GOPROXY="https://goproxy.cn,direct" 

3.从Github中导入项目到GOLAND

  • 在Welcome界面时,点击Get From VCS

  • 在打开项目的界面时,点击VCS->Get form Version Control

  • 点击Repositroy URL,可以直接使用Git Url克隆项目。

  • 配套实例代码

  • 也可以点击github,授权登录之后克隆自己账号下的仓库。

导入项目后,可以在项目文件夹下打开Terminal执行以下命令:

go run example/01-hello/main.go

当屏幕上显示出hello world后,说明导入成功。


4. 基础语法

GO的源码组织方式

  • GO通过package来组织源码。
package 包名

源码必须有所属的包。并且,一个源码仅能从属于一个包。

  • 可执行程序的包名必须为main,且必须包含main函数。
  • 可通过import语句导入包。

Hello World

package main  

//导入标准库的fmt包,这个包主要用于进行格式化I/O操作
import (  
   "fmt"  
)  
  
func main() {  
//GO语言不需要使用分号作为语句结束符,除非你打算将两个语句写在同一行
   fmt.Println("hello world")  //与JAVA Sysout函数类似。

}

变量和常量

  • GO语言是一种强类型语言,一旦某一个变量被定义类型,如果不经过强制转换(或隐式转换),那么它的变量类型将不会改变。
  • 和JS类似,GO语言可以无需明确指定变量类型,此时,变量的类型将会交由Go推导。当然,也可以显示地声明。变量确定类型后,不能再变更其类型
  • Go语言还有一个特别的规则,如果声明的变量没有被使用,则会抛出Error而非Warning。
  • 与其他众多语言不同,Golang的变量类型声明是后置的
//不指定变量类型 var [变量名] = [变量值]
var a = "initial"  
var d = true  
//指定变量类型  var [变量名] [数据类型] = [变量值]
var b, c int = 1, 2  
var e float64  
//省略var的声明 [变量名] := [变量值]
f := float32(e)  
  
g := a + "foo"  
fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0  
fmt.Println(g)                // initialapple  

//常量的声明与变量类似,var改成const
const s string = "constant"  
const h = 500000000  
const i = 3e20 / h  
//打印多个数据时,会自动用空格隔开。
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
//constant 500000000 6e+11 -0.28470407323754404 0.7591864109375384


数据类型

Go语言共有四大类数据类型:

类型描述
布尔型true or false
数字类型Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
字符串类型字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
派生类型包括:(a) 指针类型(Pointer) (b) 数组类型 (c) 结构化类型(struct) (d) Channel 类型(e) 函数类型 (f) 切片类型 (g) 接口类型(interface)(h) Map 类型

具体参考:Go 语言数据类型 | 菜鸟教程

分支结构

if-else

  • Go语言中,判断条件语句不需要加括号。特别的,Go语言不允许判断条件后面不接大括号直接写执行语句。
  • Go语言中,if语句允许在进行判断之前,先进行变量的初始化。(类似for循环中,第一个语句是初始化语句)
if 7%2 == 0 {  
   fmt.Println("7 is even")  
} else {  
   fmt.Println("7 is odd")  
}  
//第一个语句是初始化,第二个才是判断条件
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")  
}

Switch

  • Go语言中,不需要显式地为每个分支加上一条break语句。
  • Go语言中的Switch非常强大,可以使用任意数据类型作为判断变量,也可以不接变量名,直接写大括号,来取代if-else语句
a := 2  
switch a {  
case 1:  
   fmt.Println("one")  
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()
//和if-else语句类似
switch {  
case t.Hour() < 12:  
   fmt.Println("It's before noon")  
default:  
   fmt.Println("It's after noon")  
}

循环结构

  • Go语言中,仅有for一种循环。
  • Go语言支持break和continue语句控制循环的退出。
i := 1  
//for后不加任何语句,代表死循环,类似while(true)
for {  
   fmt.Println("loop")  
   break  
}  
//可以使用经典C循环
for j := 7; j < 9; j++ {  
   fmt.Println(j)  
}  
for n := 0; n < 5; n++ {  
   if n%2 == 0 {  
      continue  
   }  
   fmt.Println(n)  
}  
//跟while循环是一样的
for i <= 3 {  
   fmt.Println(i)  
   i = i + 1  
}

数组

  • 数组就是一个长度确定的元素序列。
  • 声明数组时,必须指定数组类型。
//声明数组而不直接赋值
var a [5]int  
a[4] = 100  
//使用len()函数获取数组长度
fmt.Println("len:", len(a))  

/*声明数组并赋值,
注意:1)数组类型需要写在等号右边
2)数组大小需要写在类型左边。*/
b := [5]int{1, 2, 3, 4, 5}  
var c = [3]int{1, 2, 3}  

//多维数组
var twoD [2][3]int  

切片(slice)

  • 切片是长度可变的一种元素集合。在对切片追加元素时,切片容量会增大。
  • 如果在声明数组时,未指定它的大小,则会默认这是一个切片。
var identifier []type
  • 使用make()创建一个切片:make([]切片变量类型,切片初始长度)
s := make([]string, 3)  
s[0] = "a"  
s[1] = "b"  
s[2] = "c"  
//可以像数组一样进行访问操作
fmt.Println("get:", s[2])   // c  
fmt.Println("len:", len(s)) // 3  
  • 声明切片并赋值,和声明数组并赋值的方式很像,只是不需要声明切片大小。
s1 :=[] int {1,2,3}
  • 将数组的值赋值给切片
arr := [3]int{1, 3, 4}  
sArr0 := arr[:]  
fmt.Println(sArr0)//[1 3 4] 
//将arr中下标范围在[0,2)(左闭右开)之间元素赋值给切片sArr1.
sArr1 := arr[0:2]//[1 3]    
  • 使用append()函数追加数据
s = append(s, "d")  
s = append(s, "e", "f")  
  • 使用copy()函数复制切片
c := make([]string, len(s))  
copy(c, s)  
fmt.Println(c) // [a b c d e f]  
  • 类似Python的切片索引
//s: [a b c d e f]
fmt.Println(s[2:5]) // [c d e]  
fmt.Println(s[:5])  // [a b c d e]  
fmt.Println(s[2:])  // [c d e f]  

集合(map)

  • map是一种无序的键值对集合,类似python中的字典。

  • Map 是一种集合,所以我们可以像遍历数组和切片那样遍历它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

  • 使用make()声明空map:make(map[key类型]value类型)

m := make(map[string]int)  
  • 使用 id[key] = value 写入键值对
m["one"] = 1  
m["two"] = 2  
  • 如果访问了不存在的key,则会返回value类型的默认值,int是0,string是nil(相当于不输出)
fmt.Println(m["unknown"]) // 0  

m1 := make(map[int]string)  
m1[1] = "one"  
fmt.Println(m1[1])  //one
fmt.Println("m1[3]:",m1[3])//m1[3]:
  • 使用map的value来进行赋值操作。
//这种写法在Golang中被称为逗号ok模式('comma ok' idiom),用以表示如果r存在,则ok为true。
r, ok := m["unknown"]  
fmt.Println(r, ok) // 0 false  
  • 通过delete()函数删除键值对,需要传入map ID和key。
delete(m, "one")  
  • 代码注释中提到的'comma ok' idiom在Golang中还有许多应用,参考golang逗号模式
  • 简单来说,可以用于判断map中是否存在对应的键值、接口类型变量是否包含指定类型、通道是否关闭。
  • 本质上而言,'comma ok' idiom意味着语句得到了双返回值。第一个返回值是真正的返回值,第二个返回值则是一些判定。通常会在定义函数时定义双返回值用以传递函数是否正常执行的讯息。

范围(range)

  • Go 语言中 range 关键字用于 for 循环中遍历数组(array)、切片(slice)、通道(channel)或集合(map)的元素。类似于java语言的增强for循环和c#的foreach循环。
  • 在数组和切片中它返回元素的索引和索引对应的值.
nums := []int{2, 3, 4}  
sum := 0  
//需要声明两个变量来存放range的返回值。前者为索引,后者为对应值
for i, num := range nums {  
   sum += num  
   if num == 2 {  
      fmt.Println("index:", i, "num:", num) // index: 0 num: 2  
   }  
}  
fmt.Println(sum) // 9  
  • 当然,也可以只使用一个返回值。不使用的值需要使用下划线_占位。
  • 如果只使用第一个返回值(索引),则可以不使用下划线占位。
//
for _, n := range nums {  
   fmt.Println(n)  
}
for i:= range nums {  
   fmt.Println(n)  
}
  • 在map中,range返回键值对。
m := map[string]string{"a": "A", "b": "B"}  
for k, v := range m {  
   fmt.Println(k, v) // b 8; a A  
}  
for k := range m {  
   fmt.Println("key", k) // key a; key b  
}

函数

  • Golang的函数原生支持多返回值。多返回值需要使用括号括起,并为每个返回值设定类型。

  • Golang的函数只要在文件内或在import内,则可以在任一位置调用此函数,而无需在代码首加入函数声明(如C/C++,C99、C11标准不允许不声明函数直接使用)

  • 单返回值的函数声明

func add(a int, b int) int {  
   return a + b  
}  

//如果参数类型一致,则可以省略前面的类型声明
func add2(a, b int) int {  
   return a + b  
}  
  • 多返回值的函数声明
//此例中,第一个值为真正的返回值,第二个值则为'comma ok' idiom下的ok信息。
func exists(m map[string]string, k string) (v string, ok bool) {  
   v, ok = m[k]  
   return v, ok  
}  
  
func main() {  
   v, ok := exists(map[string]string{"a": "A"}, "a")  
   fmt.Println(v, ok) // A True  
}

指针

  • Golang提供有限的指针操作。其主要应用为在函数外修改变量值。
func add2(n int) {  
//直接修改拷贝的变量,不会影响传入变量值
   n += 2  
}   

//传入变量指针
func add2ptr(n *int) {  
//解引用,对指针对应的值进行修改。
   *n += 2  
}  
  
func main() {  
   n := 5  
   add2(n)  
   fmt.Println(n) // 5  
   add2ptr(&n)  
   fmt.Println(n) // 7  
}

结构体(struct)

  • 类似c++的结构体。结构体的声明语法如下:
//user为结构体名称,name和password分别为结构体成员。
type user struct {  
   name     string  
   password string  
}
  • 结构体的初始化方法如下:
func main() {  
//可以显示地指定值对应的成员,也可以不指定,按顺序赋值。
   a := user{name: "wang", password: "1024"}  
   b := user{"wang", "1024"}  
   c := user{name: "wang"}  
   //在声明结构体变量之后,同样可以使用C式的“.”直接访问成员并赋值。
   c.password = "1024"  
   var d user  
   d.name = "wang"  
   d.password = "1024"  
}  

  • 结构体同样可以作为函数参数,同样存在指针用法。
func checkPassword(u user, password string) bool {  
   return u.password == password  
}  
//使用结构体指针作为函数参数,在大型结构体下可以节省一定的开销。
func checkPassword2(u *user, password string) bool {  
   return u.password == password  
}

结构体方法

  • golang中,结构体同样允许存在方法成员。但是与C++、JAVA等语言不同的是,golang的结构体方法是需要定义在结构体之外的。
type user struct {  
   name     string  
   password string  
}  
//定义结构体方法的语法:在func后、标识符前,加上一个(结构体变量名 结构体名)
func (u user) checkPassword(password string) bool {  
   return u.password == password  
}  
  
func (u *user) resetPassword(password string) {  
   u.password = password  
}  

错误处理

  • 在golang,抛出异常的方式通常是使用一个error类型返回值传递。如果它等于nil,则证明函数没有抛出异常,反之亦然。这个返回值可以被打印出来,也可以作为if-else的判断条件。
  • golang的这种异常处理方式可以明确的知道是哪个函数抛出的错误。
func findUser(users []user, name string) (v *user, err error) {  
   for _, u := range users {  
      if u.name == name {  
      //如果正常返回,则正常传递返回值,err设定为nil
         return &u, nil  
      }  
   }  
   //如果出现异常,则返回nil,并使用errors.New()来抛出错误提示
   return nil, errors.New("not found")  
}

使用前文提到的if种嵌套初始化语句的写法处理异常:

if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {  
   fmt.Println(err) // not found  
   return  
} else {  
   fmt.Println(u.name)  
}

字符串操作

对字符串a := "hello",可进行如下操作:

查找子串

fmt.Println(strings.Contains(a, "ll"))//true

子串计数

fmt.Println(strings.Count(a, "l")) //2
fmt.Println(strings.Count(a, "ll")) //1

串首匹配

fmt.Println(strings.HasPrefix(a, "he"))               // true

串尾匹配

fmt.Println(strings.HasSuffix(a, "llo"))              // true

获取子串索引

Index函数首先查找主串中是否包含子串,如包含,则返回子串第一个字符的索引。

fmt.Println(strings.Index(a, "ll"))   //2

字符串连接

Join函数的第一个参数是string类型的切片。

fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo

重复字符串

fmt.Println(strings.Repeat(a, 2))  //hellohello

子串替换

func Replace(s, old, new string, n int) string Replace 返回主串 s 的副本,其中前 n 个不重叠的 old 子串替换为 new子串。

  • 如果 old 为空,它将从字符串的开始匹配,并将子串按顺序插入到原串中。
  • 如果 n < 0,则替换次数没有限制。
fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
fmt.Println(strings.Replace("i am geeks", "", "G", 5))
//GiG GaGmG geeks

字符串分割

返回string类型切片。

fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]

大小写转换

fmt.Println(strings.ToLower(a))                       // hello
fmt.Println(strings.ToUpper(a))                       // HELLO

字符串长度

一个中文对应3个字符。

fmt.Println(len(a))                                   // 5  
b := "你好"  
fmt.Println(len(b)) // 6

格式化输出printf

  • 与其他语言不同的是,golang的占位符通常只需要使用%v即可代表所有的数据类型。但是如果需要保留小数位数时,则需要使用到%f。
s := "hello"  
n := 123
fmt.Printf("s=%v\n", s)  // s=hello  
fmt.Printf("n=%v\n", n)  // n=123
f := 3.141592653  
//保留2位小数
fmt.Printf("%.2f\n", f) // 3.14
  • 可以使用+、#等符号详细输出结构体的信息。
type point struct {  
   x, y int  
}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}  
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

json操作

  • 在golang中,如果字段首字母是大写,则表明其是公开的(public)
  • 只要结构体存在公开字段,就可以使用json.Marshal方法序列化为json
type userInfo struct {  
   Name  string  
   //如果想在json输出时字段改名,则在类型后面加上这样一句
   Age   int `json:"age"`  
   Hobby []string  
}
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}  
buf, err := json.Marshal(a)
if err != nil {  
//使用panic后将不会执行后续代码
   panic(err)  
}  
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
  • 当然,在序列化结构体时,也可以让其格式化。此时需要调用MarshalIndent方法。输出中的每个 JSON 元素都将在以前缀(prefix)开头的新行开始,后跟一个或多个缩进副本(indent)。
func MarshalIndent(v any, prefix, indent string)([]byte, error)

buf, err := json.MarshalIndent(a, "", "\t")  
if err != nil {  
   panic(err)  
}  
fmt.Println(string(buf))

输出:

{
        "Name": "wang",
        "age": 18,
        "Hobby": [
                "Golang",
                "TypeScript"
        ]
}

我们可以使用Unmarshal方法将json解组
注意,参数2的值必须为指针。

func Unmarshal(data []byte, v any) error
var b userInfo  
err = json.Unmarshal(buf, &b)  
if err != nil {  
   panic(err)  
}  
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}

时间处理

golang time常用方法详解_Golang_脚本之家 (jb51.net) 最常用的是获取当前时间:

now := time.Now()

获取指定时间:

//参数分别是:年,月,日,时,分,秒,纳秒,时区
t4 := time.Date(2019, 9, 30, 14, 28, 26, 23, time.Local)  // 返回时间格式Time

获取当前时间戳:

t1 := time.Now()
t1Second := t1.Unix()  // 获取秒的时间戳
fmt.Println(t1Second)
t1Nano := t1.UnixNano()    // 获取毫秒时间戳
fmt.Println(t1Nano)

时间戳转时间:

  • 注意,golang中时间格式默认为2006-01-02 15:04:05
timeStamp := 1569826535
t := time.Unix(int64(timeStamp), 0)  // time.Time
//按照时间格式转换为Time类型
strTime := t.Format("2006-01-02 15:04:05")
fmt.Println(strTime)  // 2019-09-30 14:55:35

时间字符串转时间:

s := "2019年9月30日 15:10:30"  // ---> 2019-09-30 15:10:30
t, err := time.Parse("2006年1月2日 15:4:5", s)
if err != nil {
	fmt.Println(err.Error())
}
result := t.Format("2006-01-02 15:04:05")
fmt.Println(result)

字符串和数字转换

字符串转数字:

//参数分别为:字符串、进制、数字
n, _ := strconv.ParseInt("111", 10, 64)  
//转浮点数:
f, _ := strconv.ParseFloat("1.234", 64)  

将十进制字符串转为数字:

n2, _ := strconv.Atoi("123")  
fmt.Println(n2) // 123
//如果输入不合法,则将会错误传给err参数。
n2, err := strconv.Atoi("AAA")  

将十进制数字转为字符串:

  • 注意,Itoa方法没有err返回值
n3 := strconv.Itoa(1234)  
fmt.Println(n3)