Go语言基础 | 青训营笔记

33 阅读10分钟

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

Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(静态编译)。

我们以经典的hello,world作为入门

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

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

main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main 函数 也很特殊,它是整个程序执行时的入口。main函数所做的事情就是程序做的。当然了,main函数一般调用其它包里的函数完成很多工作

必须告诉编译器源文件需要哪些包,这就是跟随在package声明后面的import声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。

一个函数的声明由func关键字、函数名、参数列表、返回值列表(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析

Go语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式(并且这个格式化的工具没有任何可以调整代码格式的参数),并且go工具中的fmt子命令会对指定包,否则默认为当前目录中所有.go源文件应用gofmt命令。

程序结构

命名

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。并且大小写敏感。

Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。

break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

内建常量: true false iota nil
​
内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error
​
内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

此外,Go语言程序员推荐使用驼峰式命名。

声明与变量

Go语言的声明有以下4种

  • var:变量
  • const:常量
  • type:类型
  • func:函数实体对象的声明

使用var进行声明

变量可以这样进行声明

var 变量名 类型 = 表达式

其中类型或者表达式可以省略其一,因为不管是省略前者还是后者,Go语言都有他自己的方法。

省略类型信息

类型信息会根据其表达式对类型进行推导

省略表达式

如果是数值类型变量,那么将被声明为0(int => 0)

如果是布尔类型变量,那么将被声明为0(bool => false)

如果是字符串类型变量,那么将被声明为0(string => "")

如果是接口或引用类型变量,那么将被声明为0(slice,map,chan,指针,函数 => nil)

简短变量声明语句

在函数内部,还有一种声明

名字 := 表达式

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

package main
​
import (
    "fmt"
    "math"
)
​
func main() {
​
    var a = "initial"var b, c int = 1, 2var d = truevar e float64   //0
​
    f := float32(e) //0
​
    g := a + "foo"
    fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
    fmt.Println(g)                // initialappleconst 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更像是Python和C语言的结合,语法相似,基础语法理解起来并不难,性能强大,并且很多功能都类似,比如C/C++特色的指针,这里也有类似的体现

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。

如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p表达式对应p指针指向的变量的值。一般*p表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。

x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

new函数

表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

new函数类似一个语法糖,并且每次调用都会返回一个新的地址

p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

基础数据类型

类C,其中常见到的8,16,32,64指的是8,16,32,64bit大小的有符号整数

复合数据类型

数组

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。

//初始化1
var q [3]int = [3]int{1, 2, 3}
​
//初始化2
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已,会进行自动扩容操作,每一次容量的变化都会导致重新分配内存和copy操作。

//初始化
var runes []rune
​
//使用range来遍历,对于数组会返回两个值 第一个是下标 第二个是数值 如果不需要索引可以用_
for _, r := range "Hello, 世界" {
    //append(原数组,...) 可以追加多个数
    runes = append(runes, r)
}
​
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
​
​
//内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。
s := make([]string, 3)

map

无序的key/value对的集合,其中所有的key都是不同的

//创建方式
m1 := make(map[string]int)
​
//赋值方式
m1["one"] = 1
m1["two"] = 2
​
m2 := map[string]int{"one": 1, "two": 2}
​
var m3 = map[string]int{"one": 1, "two": 2}
​
​
//删除数据
delete(m1, "one")
fmt.Println(m) //map[two:2]
​
​

而且x += yx++等简短赋值语法也可以用在map上

ages["bob"] += 1 
ages["bob"]++

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作

结构体

//声明
type Employee struct {
    ID        int
    Name      string
    DoB       time.Time
    Salary    int
    Position  string
}
​
//声明了一个Employee类型的变量dilbert
var dilbert Employee
​
//dilbert结构体变量的成员可以通过点操作符访问和赋值
dilbert.Salary -= 5000 
//或者是对成员取地址,然后通过指针访问
position := &dilbert.Position
*position = "Senior " + *position 

JSON

package main
​
import (
   "encoding/json"
   "fmt"
)
​
type userInfo struct {
   Name  string
   Age   int `json:"age"`//该步骤可以更改json中的名称
   Hobby []string
}
​
func main() {
   a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
   buf, err := json.Marshal(a)
   if err != nil {
      panic(err)
   }
   fmt.Println(buf)         //16进制编码 [123 34 78 97...]
   fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
​
   buf, err = json.MarshalIndent(a, "", "\t")
   if err != nil {
      panic(err)
   }
   fmt.Println(string(buf))
​
   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"}}
}

函数

//后置数据类型
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
}

错误处理

使用一个单目的返回值来传递错误信息,go语言的错误处理(使用if处理)能够及时处理,并且能够很清晰的知道哪个函数出错了

package main
​
import (
   "errors"
   "fmt"
)
​
type user struct {
   name     string
   password string
}
​
func findUser(users []user, name string) (v *user, err error) {
   for _, u := range users {
      if u.name == name {
         return &u, nil
      }
   }
   return nil, errors.New("not found")
}
​
func main() {
   u, err := findUser([]user{{"wang", "1024"}}, "wang")
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println(u.name) // wang
​
   if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
      fmt.Println(err) // not found
      return
   } else {
      fmt.Println(u.name)
   }
}

字符串操作

package main
​
import (
   "fmt"
   "strings"
)
​
func main() {
   a := "hello"
   fmt.Println(strings.Contains(a, "ll"))                // true
   fmt.Println(strings.Count(a, "l"))                    // 2
   fmt.Println(strings.HasPrefix(a, "he"))               // true
   fmt.Println(strings.HasSuffix(a, "llo"))              // true
   fmt.Println(strings.Index(a, "ll"))                   // 2
   fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
   fmt.Println(strings.Repeat(a, 2))                     // hellohello
   fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
   fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
   fmt.Println(strings.ToLower(a))                       // hello
   fmt.Println(strings.ToUpper(a))                       // HELLO
   fmt.Println(len(a))                                   // 5
   b := "你好"
   fmt.Println(len(b)) // 6
}

字符串格式化

package main
​
import "fmt"type point struct {
   x, y int
}
​
func main() {
   s := "hello"
   n := 123
   p := point{1, 2}
   fmt.Println(s, n) // hello 123
   fmt.Println(p)    // {1 2}
​
   fmt.Printf("s=%v\n", s)  // s=hello
   fmt.Printf("n=%v\n", n)  // n=123
   fmt.Printf("p=%v\n", p)  // p={1 2}
    //+v 查看较为详细的结构
   fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
    //#v 更加详细
   fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
​
   f := 3.141592653
   fmt.Println(f)          // 3.141592653
   fmt.Printf("%.2f\n", f) // 3.14
}

时间处理

package main
​
import (
   "fmt"
   "time"
)
​
func main() {
   now := time.Now()
   fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
   t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
   t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
   fmt.Println(t)                               // 2022-03-27 01:25:36 +0000 UTC
   fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
   fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
   
   //截取时间
   diff := t2.Sub(t)
   fmt.Println(diff)                           // 1h5m0s
   fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
   t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
   if err != nil {
      panic(err)
   }
   //比较的结果是值而不是地址
   fmt.Println(t3 == t)    // true
    
    
   //获取时间戳
   fmt.Println(now.Unix()) // 1648738080
}

数字解析

package main
​
import (
   "fmt"
   "strconv"
)
​
func main() {
   //字符串 => 数字
   f, _ := strconv.ParseFloat("1.234", 64)
   fmt.Println(f) // 1.234
    
    //10进制,64位
   n, _ := strconv.ParseInt("111", 10, 64)
   fmt.Println(n) // 111
​
    //0代表自动推测进制 
   n, _ = strconv.ParseInt("0x1000", 0, 64)
   fmt.Println(n) // 4096
    
   n2, _ := strconv.Atoi("123")
   fmt.Println(n2) // 123
​
   n2, err := strconv.Atoi("AAA")
   fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}

进程信息

package main
​
import (
   "fmt"
   "os"
   "os/exec"
)
​
func main() {
   //该过程需要像Args中传入参数
   // go run example/20-env/main.go a b c d
   fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
    
  //分别来获取和写入环境变量      
   fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
   fmt.Println(os.Setenv("AA", "BB"))
​
   //启动子进程和获取输入输出
   buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
   if err != nil {
      panic(err)
   }
   fmt.Println(string(buf)) // 127.0.0.1       localhost
}

以上内容参考了Go语言圣经(中文版) 以及后端专场 学习资料一】第五届字节跳动青训营中老师提供的代码