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

135 阅读8分钟

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

这是我参与「第五届青训营」伴学笔记创作活动的第一天

前言

记录go语言学习笔记,有不足的地方还请大佬们指正。
由于本人基础较为薄弱,所以笔记尽可能详细。


一、简述Go语言优点

  • 高性能、高并发:Go语言有媲美C++、Java的性能。Go语言还内置了对高并发的支持,不像很多编程语言以库的形式支持。
  • 学习简单,学习曲线平缓:Go语言的语法风格类似于C语言,并且在C语言的基础上进行了大幅度简化。比如去掉了不要的表达式括号,循环也只有for循环一种表示方法便可以实现数值、键值等各种遍历。一个Go语言学习者只需要短短一周时间从学习阶段转到真正的开发阶段,并完成一个高并发的应用程序的开发。
  • 丰富的标准库:在很多情况下,不需要借助第三方库就可以完成基础功能的开发。标准库有很高的稳健性和兼容性的保障。
  • 完整的工具链:无论是代码检查、编译等Go语言诞生之初就有。并且Go语言还内置了完整的单元测试框架。
  • 静态链接:在Go语言里,所有的编译结果都是静态链接的。只需要考虑编译后的可执行文件,不需要附加任何东西便可部署运行。在线上容器运行,线上体积可以控制得非常小。
  • 快速编译:Go语言拥有静态语言里几乎最快的编译速度
  • 跨平台:Go语言文件能在非常多种设备下运行
  • 垃圾回收:Go语言有类似于Java的垃圾回收机制,使得开发者可以更关注于业务逻辑而不是内存分配释放。

二、基础语法

  • "hello world"例子

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main,表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

  • 变量

package main

import (
   "fmt"
   "math"
)

func main() {

   var a = "initial"

   var b, c int = 1, 2

   var d = true

   var e float64

   f := float32(e)

   g := a + "foo"
   fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
   fmt.Println(g)                // initialapple

   const s string = "constant"
   const h = 500000000
   const i = 3e20 / h
   fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}

Go语言声明变量格式如下:

var 变量名字 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用变量类型对应零值初始化该变量。 零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。

Go语言另一种声明变量的方式

变量名字 := 值

变量的类型根据表达式来自动推导。

Go语言中常量的声明方式就是把var改成const。并且在Go语言里的常量,它没有确定的类型,会根据使用的上下文来自动确定类型。

  • if-else

if 7%2 == 0 {
   fmt.Println("7 is even")
} else {
   fmt.Println("7 is odd")
}

if 8%4 == 0 {
   fmt.Println("8 is divisible by 4")
}

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")
}

Go语言里面的if-else写法与C和C++类似,但是有两个不同点:

1、 Go语言中if后面没有括号。如果你写括号的话,那么保存的时候编译器会自动把括号去掉。

2、 Go语言中的if,它必须后面接大括号,不可以像C和C++一样,直接把if里面的语句写在同一行。

  • 循环

i := 1
for {
   fmt.Println("loop")
   break
}
for j := 7; j < 9; j++ {
   fmt.Println(j)
}

for n := 0; n < 5; n++ {
   if n%2 == 0 {
      continue
   }
   fmt.Println(n)
}
for i <= 3 {
   fmt.Println(i)
   i = i + 1
}

最简单的for循环就是什么都不写,代表一个死循环。循环途中可以用break跳出,也可以使用经典的C循环。

  • switch

Go 语言中的的switch分支结构看起来也和C或者C++比较类似。但是Go语言中的switch分支结构与C风格中的有以下不同点:
1、 switch后面的变量名不需要括号。
2、 在C++里面,switch case如果不显示地加break的话该程序会往下跑完所有的case,在Go语言里面的话是不需要加break的。
3、 Go语言里面的switch功能更强大,可以使用任意的变量类型,甚至可以用来取代if-else语句。可以在switch语句后面不加任何变量,然后在case里面写条件分支。这样的代码比简单地多用个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()
switch {
case t.Hour() < 12:
   fmt.Println("It's before noon")
default:
   fmt.Println("It's after noon")
}
  • 数组

数组就是一个具有编号且长度固定的元素序列。 不过在真实业务代码里,我们很少用直接使用数组,因为数组的长度是固定的,我们通常都是用切片。

var a [5]int
a[4] = 100
fmt.Println("get:", a[2])
fmt.Println("len:", len(a))

b := [5]int{1, 2, 3, 4, 5}
fmt.Println(b)

var twoD [2][3]int
for i := 0; i < 2; i++ {
   for j := 0; j < 3; j++ {
      twoD[i][j] = i + j
   }
}
fmt.Println("2d: ", twoD)
  • 切片

切片不同于数组,切片可以任意更改长度。
切片可以使用append增加元素。在append用法中,我们必须把append的结果赋值为原数组。因为slice的原理是它存储了一个长度和一个容量,加一个指向数组的指针。当我们执行append的时候,若容量不够,会返回一个新的slice。

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

s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]

c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [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]

good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]
  • Map

我们可以使用make来创建一个map。这里需要两个类型,第一个是key类型,第二个是value类型。我们可以从里面去存储或者取出键值对。可以delete从里面删除键值对。Golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。

m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m)           // map[one:1 two:2]
fmt.Println(len(m))      // 2
fmt.Println(m["one"])    // 1
fmt.Println(m["unknow"]) // 0

r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false

delete(m, "one")

m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
  • range

对于一个slice或者map的话,我们可以用range来快速遍历。
使用range遍历的时候,对于数组,返回两个值,第一个是索引,第二个是值。
对于map,返回两个值,第一个key,第二个是value。

nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
   sum += num
   if num == 2 {
      fmt.Println("index:", i, "num:", num) // index: 0 num: 2
   }
}
fmt.Println(sum) // 9

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里面的函数原生支持返回多个值。在实际业务逻辑代码里面几乎所有的函数都返回两个值,一个是真正的返回结果,第二个值是错误信息。

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

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

func exists(m map[string]string, k string) (v string, ok bool) {
   v, ok = m[k]
   return v, ok
}

func main() {
   res := add(1, 2)
   fmt.Println(res) // 3

   v, ok := exists(map[string]string{"a": "A"}, "a")
   fmt.Println(v, ok) // A True
}
  • 指针

Golang里的指针与C风格中的几乎一样,这里略。

  • 结构体

关于初始化结构体变量:我们可以在构造的时候直接传入每个字段的初始值。也可以用键值对的方式去指定初始值,这样可以只对一部分字段进行初始化。

结构体方法:在Golang里面我们可以为结构体定义一些方法。会有一点类似其他语言里的类成员函数。 具体实现见下面的checkPassword和resetPassword方法。

命名规则就是将结构体参数类型写在函数名称前面。
实现结构体有两种方法,一种是带指针的,一种是不带指针的。它们的区别是如果该方法是带指针的话,那么我们就可以对这个结构体进行修改。如果不带指针的话,我们实际操作的是一个拷贝,我们就无法对结构体进行修改。

package main

import "fmt"

type user struct {
   name     string
   password string
}

func (u user) checkPassword(password string) bool {
   return u.password == password
}

func (u *user) resetPassword(password string) {
   u.password = password
}

func main() {
   a := user{name: "wang", password: "1024"}
   a.resetPassword("2048")
   fmt.Println(a.checkPassword("2048")) // true
}
  • 错误处理