走进Go语言的地基

78 阅读17分钟

1.1 什么是Go语言

  1. 高性能,高并发
  2. 语法简单,学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

1.2 哪些公司在使用Go语言

  • 字节跳动
  • Google 谷歌
  • 腾讯
  • 美团
  • 滴滴
  • 百度
  • Facebook
  • 七牛云
  • 哔哩哔哩
  • PingCAP

1.3 字节跳动为什么选择Golang

  1. 最初使用的是python,后来由于性能的问题换成了Go
  2. C++不太适合在线Web业务
  3. 早期团队非java背景
  4. 性能比较好
  5. 部署简单,学习成本低
  6. 内部RPC和HTTP框架的推广

2.1 开发环境-安装Golang

首先安装GolangSDK,配置集成开发环境。

  • VSCode
  • goland
  • Sublime Text
  • Vim
  • Emacs

2.2 基础语法 - Hello World

`package main

import ("fmt")

func main(){

fmt.Println("hello world")

}`

第一行代码定义了一个包(package),在Go语言中,每个程序都必须属于一个包。main是一个特殊的包,它定义了一个可执行程序的入口点。

第二行代码导入了一个名为fmt的包,它提供了格式化输入输出的功能。在这个程序中,我们使用了fmt.Println函数来打印输出。

最后几行代码定义了一个名为main的函数,它是程序的入口点。使用了fmt.Println函数来打印输出字符串"hello world"。Println函数会在控制台中打印出指定的内容,并在最后自动添加一个换行符。

2.3 基础语法

2.3.1 变量

package main
import (
"fmt"
"math"
)
func main(){
var temp string = "variety"
message := "hello"
fmt.Println(temp)
fmt.Println(message)
}

正常的变量声明语法是使用关键字var,使用短变量声明语法,我们可以简化这个过程,只需要写变量名和初始值,而不需要显式地指定变量的类型:message := "Hello, World!" 这里的:=是短变量声明,它会推断出变量的类型并进行声明,并将初始值赋给该变量。

需要注意的是,短变量声明只能用于函数内部,用于声明局部变量。如果你想在函数外部声明变量,或者需要指定变量的类型,你应该使用正常的变量声明语法。

2.3.2 if-else

package main
import (
"fmt
)
func main() {
   var flag = true
   message := "hello"
   number := 999
   fmt.Println(flag)
   fmt.Println(message)
   if number == 99 {
      fmt.Println(number)
   } else {
      fmt.Println("hahahah")
   }
}

不同于其他的编程语言,Golang中的if判断条件不需要加括号,但需要加花括号进行限定,当条件为真时执行的具体语句。

2.3.3 switch


package main

import "fmt"

func main() {
   day := 4

   switch day {
   case 1:
      fmt.Println("星期一")
   case 2:
      fmt.Println("星期二")
   case 3:
      fmt.Println("星期三")
   case 4:
      fmt.Println("星期四")
   case 5:
      fmt.Println("星期五")
   case 6:
      fmt.Println("星期六")
   case 7:
      fmt.Println("星期日")
   default:
      fmt.Println("无效的日期")
   }
}

Go语言的switch语句中,默认的行为就是自动地终止当前case的执行,无需额外的break语句。 这种特性使得Go语言的switch语句更加简洁和易读,避免了因忘记或错误使用break导致的逻辑错误。 Golang的switch中的case后不需要加break,而java等语言需要加break,有时漏加便会出现错误。

2.3.4 数组

package main
import "fmt"   
func main() {
   // 声明一个包含5个整数的数组
    var numbers [5]int
   // 给数组赋值   
    numbers[0] = 10
    numbers[1] = 20
    numbers[2] = 30
    numbers[3] = 40
    numbers[4] = 50
    //输出10和30
    fmt.Println(numbers[0]) 
    fmt.Println(numbers[2])    
    //声明并打印数组元素
    names := [3]string{"Alice", "Bob", "Charlie"}   
    fmt.Println(names[0]) 
    fmt.Println(names[2]) 
}

Golang的数组和其他语言的数组大同小异,接下来介绍的Golang中的切片就和数组有一定的区别。

2.3.5 切片

package main

import "fmt"

func main() {
   // 创建一个切片
   numbers := []int{1, 2, 3, 4, 5}

   // 打印切片的长度和容量
   fmt.Println("长度:", len(numbers)) // 输出:5
   fmt.Println("容量:", cap(numbers)) // 输出:5

   // 切片操作
   sliced := numbers[1:4] // 切割索引1到3的元素
   fmt.Println(sliced)    // 输出:[2 3 4]

   // 修改切片元素
   sliced[0] = 10
   fmt.Println(numbers) // 输出:[1 10 3 4 5]

   // 使用make函数创建切片
   fruits := make([]string, 3) // 创建一个长度为3的字符串切片
   fruits[0] = "apple"
   fruits[1] = "banana"
   fruits[2] = "orange"
   fmt.Println(fruits) // 输出:[apple banana orange]
}

切片(slice)是一种动态数组,它提供了对数组的部分或全部元素的引用。切片是一个引用类型,它包含了一个指向数组的指针、切片的长度和容量。

与数组相比,切片具有更灵活的长度和容量,可以根据需要动态调整。切片可以通过对现有数组或切片进行切割来创建,也可以使用内置的make函数来创建。

需要注意的是,切片是对底层数组的引用,因此对切片的修改会影响到底层数组的对应元素。

Golang中数组和切片的区别如下

  1. 长度的灵活性:数组的长度是固定的,在声明时需要指定长度。而切片的长度是可变的,它可以根据需要动态增长或缩小。
  2. 容量的概念:数组的容量就是它的长度,即数组声明时指定的大小。而切片的容量是指它底层数组的长度,可以通过cap()函数获取。切片的容量可以超过其实际长度,这使得切片可以在需要时动态增长。
  3. 内存分配方式:数组在声明时就会分配一段连续的内存空间。而切片只是对底层数组的一个引用,可以共享同一块底层数组的内存。
  4. 值传递 vs 引用传递:数组是值类型,当将一个数组赋值给另一个数组时,会创建一个新的数组并将值复制给它。切片是引用类型,当将一个切片赋值给另一个切片时,它们将引用同一块底层数组。
  5. 动态性:切片具有动态性,可以根据需要动态地增加或缩小长度。而数组的长度是固定的,无法直接改变。
  6. 参数传递:切片通常在函数间传递时使用较多,因为它们可以动态地改变大小并且不会复制整个数组的数据。

2.3.6 map

package main

import "fmt"

func main() {
   // 创建一个空的map
   var studentScores map[string]int
   fmt.Println(studentScores) // 输出:map[]

   // 使用make函数创建map
   studentScores = make(map[string]int)

   // 插入键值对
   studentScores["Alice"] = 95
   studentScores["Bob"] = 87
   studentScores["Charlie"] = 92

   // 通过键访问值
   fmt.Println(studentScores["Alice"]) // 输出:95

   // 修改值
   studentScores["Bob"] = 90

   // 删除键值对
   delete(studentScores, "Charlie")

   // 遍历map
   for name, score := range studentScores {
      fmt.Println(name, score)
   }
}

map是一种无序的键值对集合,也称为字典或关联数组。map提供了快速的查找、插入和删除操作。

map的定义语法为:map[keyType]valueType,其中keyType是键的类型,valueType是值的类型。

2.3.7 range

package main

import "fmt"

func main() {
   // 迭代数组
   numbers := [5]int{1, 2, 3, 4, 5}
   for index, value := range numbers {
      fmt.Println(index, value)
   }

   // 迭代切片
   fruits := []string{"apple", "banana", "orange"}
   for index, value := range fruits {
      fmt.Println(index, value)
   }

   // 迭代字符串
   str := "Hello, World!"
   for index, value := range str {
      fmt.Println(index, string(value))
   }

   // 迭代map
   studentScores := map[string]int{
      "Alice":   95,
      "Bob":     87,
      "Charlie": 92,
   }
   for name, score := range studentScores {
      fmt.Println(name, score)
   }

   // 迭代通道
   numbersChan := make(chan int)
   go func() {
      defer close(numbersChan)
      for i := 1; i <= 5; i++ {
         numbersChan <- i
      }
   }()
   for num := range numbersChan {
      fmt.Println(num)
   }
}

range关键字用于迭代数组、切片、字符串、map以及通道等数据结构。

range关键字提供了一种简洁的方式来遍历这些数据结构的元素。它返回两个值,第一个值是元素的索引(或键),第二个值是该索引(或键)对应的元素的值。在迭代时,range会自动为每个元素生成索引,并将索引和值赋值给迭代变量。

2.3.8 函数

package main

import "fmt"

func main() {
   // 调用加法函数
   fmt.Println(add(3, 5)) // 输出:8

   // 调用减法函数
   fmt.Println(subtract(10, 4)) // 输出:6
}

// 加法函数
func add(a, b int) int {
   return a + b
}

// 减法函数
func subtract(a, b int) int {
   return a - b
}

函数是每个编程语言都有的,Golang的函数在声明上对比其他语言比较简洁,使用上基本没有区别。

2.3.9 指针

package main

import "fmt"

func main() {
   // 定义一个整数变量
   num := 10
   fmt.Println("变量的值:", num)   // 输出:10
   fmt.Println("变量的地址:", &num) // 输出:0xc0000140a8

   // 定义一个指针变量并指向num的地址
   numPtr := &num
   fmt.Println("指针变量的值:", numPtr)    // 输出:0xc0000140a8
   fmt.Println("指针变量指向的值:", *numPtr) // 输出:10

   // 修改指针指向的值
   *numPtr = 20
   fmt.Println("修改后的值:", num) // 输出:20

   // 将指针作为参数传递给函数
   modifyValue(numPtr)
   fmt.Println("函数执行后的值:", num) // 输出:30
}

// 函数修改指针指向的值
func modifyValue(ptr *int) {
   *ptr = 30
}

指针是一种特殊的数据类型,用于存储变量的内存地址。指针变量可以指向其他变量的内存地址,并允许直接访问和修改该地址上存储的值。

使用指针可以在函数间传递大型的数据结构,而不需要进行数据的复制,从而提高程序的效率。此外,指针还可以用于修改函数外部的变量。

2.3.10 结构体

package main

import "fmt"

// 定义一个结构体类型
type Person struct {
   Name   string
   Age    int
   Gender string
}

func main() {
   // 创建一个结构体实例
   person := Person{
      Name:   "Alice",
      Age:    25,
      Gender: "Female",
   }

   // 访问结构体成员变量
   fmt.Println("Name:", person.Name)
   fmt.Println("Age:", person.Age)
   fmt.Println("Gender:", person.Gender)

   // 修改结构体成员变量值
   person.Age = 26
   fmt.Println("Modified Age:", person.Age)
}

结构体的特点

  1. 定义结构体:使用type关键字和struct关键字来定义结构体。例如,type Person struct { Name string; Age int }定义了一个名为Person的结构体,它有两个字段NameAge,分别是字符串类型和整数类型。
  2. 创建结构体实例:使用结构体类型和花括号来创建结构体实例。例如,person := Person{Name: "Alice", Age: 25}创建了一个名为person的结构体实例,并为其字段赋予初始值。
  3. 访问结构体字段:使用.操作符来访问结构体的字段。例如,person.Name表示访问person结构体实例的Name字段。
  4. 修改结构体字段:可以通过赋值操作来修改结构体的字段值。例如,person.Age = 26person结构体实例的Age字段修改为26。
  5. 结构体嵌套:结构体可以嵌套在其他结构体中,形成复杂的数据结构。例如,type Address struct { Street string; City string }定义了一个名为Address的结构体,它有两个字段StreetCity。然后,可以在另一个结构体中嵌套Address结构体,例如type Person struct { Name string; Age int; Address Address }
  6. 匿名字段:结构体字段可以是匿名的,即没有字段名,只有字段类型。这样的字段可以直接通过结构体实例访问。例如,type Person struct { string; int }定义了一个名为Person的结构体,它有两个匿名字段,分别是字符串类型和整数类型。
  7. 指针类型的结构体:可以使用指针类型来创建结构体的指针。例如,personPtr := &person创建了一个指向person结构体实例的指针。

2.3.11 结构体方法

package main

import "fmt"

type Rectangle struct {
  width  float64
  height float64
}

// 定义一个结构体方法,计算矩形的面积
func (r Rectangle) Area() float64 {
  return r.width * r.height
}

// 定义一个结构体方法,计算矩形的周长
func (r Rectangle) Perimeter() float64 {
  return 2 * (r.width + r.height)
}

func main() {
  // 创建一个矩形实例
  rectangle := Rectangle{
     width:  10,
     height: 5,
  }

  // 调用结构体方法计算矩形的面积和周长
  area := rectangle.Area()
  perimeter := rectangle.Perimeter()

  // 打印输出结果
  fmt.Println("Area:", area)
  fmt.Println("Perimeter:", perimeter)
}

结构体方法特点

  1. 封装性:结构体方法可以访问结构体的私有成员变量和方法,因此可以进行封装操作,限制对内部数据的直接访问。
  2. 关联性:结构体方法通过关联到结构体上,可以对结构体的属性和行为进行操作和修改,提高了代码的可读性和可维护性。
  3. 值接收者与指针接收者:结构体方法可以定义为值接收者(Value Receiver)或指针接收者(Pointer Receiver)。值接收者使用结构体的副本进行操作,而指针接收者使用结构体的指针进行操作。值接收者适用于结构体对象较小且不需要修改的情况,而指针接收者适用于需要修改结构体数据或避免复制大量数据的情况。
  4. 可调用性:结构体方法可以像函数一样被调用,通过结构体实例.方法名()的方式调用。这种调用方式使得方法与结构体的关联性更加明显,可以提高代码的可读性。
  5. 继承性:结构体方法可以被嵌入到其他结构体中,实现方法的继承和重用。嵌入到其他结构体中的方法可以直接调用外部结构体的方法和成员变量。

2.3.12 错误处理

在Go语言中,错误处理是一种重要的机制,用于处理和传递程序中的错误信息。Go通过使用error类型和错误返回值来处理错误。

以下是一些在Go语言中进行错误处理的常用方法:

  1. 错误类型的定义:可以使用error类型来定义错误。error是一个接口类型,它有一个名为Error()的方法,用于返回错误的描述信息。通常,错误类型是通过自定义类型实现error接口来定义的。例如,可以定义一个名为MyError的错误类型,实现error接口的Error()方法,用于返回错误信息。
type MyError struct {
    errorMsg string
}

func (e MyError) Error() string {
    return e.errorMsg
}
  1. 函数返回错误:当函数可能发生错误时,可以将错误作为函数的返回值进行返回。通常情况下,函数的最后一个返回值是一个error类型。如果函数执行过程中发生错误,可以使用errors.New()函数创建一个新的错误实例,并返回该错误值。
import (
    "errors"
    "fmt"
)

func divide(x, y float64) (float64, error) {
    if y == 0 {
        return 0, errors.New("division by zero")
    }
    return x / y, nil
}
  1. 错误检查:在接收函数返回的错误时,通常需要对错误值进行检查。可以使用条件语句(如if语句)来检查错误值是否为nil,以确定函数是否执行成功。如果错误值不为nil,则表示函数执行过程中发生了错误,可以根据错误信息采取相应的处理措施。
result, err := divide(10, 0)
if err != nil {
    fmt.Println("Error:", err)
} else {
    fmt.Println("Result:", result)
}
  1. 错误传递:在多层函数调用中,如果在内部函数中发生了错误,可以将错误传递给调用方进行处理。可以通过将错误作为返回值在函数之间进行传递。在每一层函数中,都可以检查上一层函数返回的错误,并根据需要进行处理或继续传递。
func process() error {
    result, err := divide(10, 0)
    if err != nil {
        return err
    }
    // 在这里继续进行其他操作
    return nil
}

需要注意的是,在Go语言中,习惯使用在函数的最后一个返回值位置返回错误。这种方式使错误处理更加清晰和简洁。此外,可以使用defer语句来延迟执行错误处理的操作,以确保在函数退出前进行必要的清理操作。

通过良好的错误处理机制,可以有效地捕获和处理程序中发生的错误,提高代码的健壮性和可靠性。

2.3.13 字符串操作

package main

import (
   "fmt"
   "strings"
)

func main() {
   str := "Hello, World!"

   // 字符串长度
   length := len(str)
   fmt.Println("字符串长度:", length)

   // 字符串拼接
   str1 := "Hello"
   str2 := "World"
   result := str1 + ", " + str2 + "!"
   fmt.Println("字符串拼接:", result)

   // 子串查找
   contains := strings.Contains(str, "World")
   fmt.Println("是否包含子串:", contains)

   // 子串索引
   index := strings.Index(str, "World")
   fmt.Println("子串索引:", index)

   // 字符串分割
   slices := strings.Split(str, ", ")
   fmt.Println("字符串分割:", slices)

   // 字符串替换
   newStr := strings.Replace(str, "World", "Golang", -1)
   fmt.Println("字符串替换:", newStr)

   // 字符串大小写转换
   lower := strings.ToLower(str)
   upper := strings.ToUpper(str)
   fmt.Println("字符串大小写转换:", lower, upper)

   // 去除首尾空格
   trimmed := strings.TrimSpace("   Hello, World!   ")
   fmt.Println("去除首尾空格:", trimmed)

   // 字符串切片操作
   subStr := str[7:12]
   fmt.Println("字符串切片:", subStr)
}

2.3.14 字符串格式化

package main

import (
   "fmt"
)

func main() {
   // 字符串占位符
   name := "Alice"
   age := 25
   fmt.Printf("My name is %s and I'm %d years old.\n", name, age)

   // 宽度和精度
   price := 59.99
   fmt.Printf("The price is %.2f dollars.\n", price)

   // 输出整数的二进制和八进制表示
   number := 42
   fmt.Printf("Binary: %b\n", number)
   fmt.Printf("Octal: %o\n", number)

   // 左对齐、右对齐、填充操作
   message := "Hello"
   fmt.Printf("Left aligned: %10s\n", message)
   fmt.Printf("Right aligned: %-10s\n", message)
   fmt.Printf("Padded: %05d\n", number)

   // 输出百分比
   discount := 0.15
   fmt.Printf("Discount: %.1f%%\n", discount*100)
}

我们使用了fmt.Printf()函数来格式化输出字符串。下面是一些常见的格式化占位符:

  • %s:字符串占位符
  • %d:有符号十进制整数占位符
  • %f:浮点数占位符
  • %b:二进制占位符
  • %o:八进制占位符

2.3.15 JSON处理

当涉及到处理JSON时,Go语言的标准库encoding/json提供了一套非常强大和灵活的工具。

package main

import (
   "encoding/json"
   "fmt"
)

type Person struct {
   Name    string   `json:"name"`
   Age     int      `json:"age"`
   Hobbies []string `json:"hobbies"`
}

func main() {
   // JSON编码
   person := Person{
      Name:    "Alice",
      Age:     25,
      Hobbies: []string{"reading", "coding", "swimming"},
   }

   jsonData, err := json.Marshal(person)
   if err != nil {
      fmt.Println("JSON编码错误:", err)
      return
   }

   fmt.Println("JSON编码结果:")
   fmt.Println(string(jsonData))

   // JSON解码
   var decodedPerson Person
   err = json.Unmarshal(jsonData, &decodedPerson)
   if err != nil {
      fmt.Println("JSON解码错误:", err)
      return
   }

   fmt.Println("JSON解码结果:")
   fmt.Println(decodedPerson)
   fmt.Println("姓名:", decodedPerson.Name)
   fmt.Println("年龄:", decodedPerson.Age)
   fmt.Println("爱好:", decodedPerson.Hobbies)
}

运行结果如图所示

屏幕截图 2023-08-23 155139.png

2.3.16 时间处理

package main

import (
   "fmt"
   "time"
)

func main() {
   // 获取当前时间
   currentTime := time.Now()
   fmt.Println("当前时间:", currentTime)

   // 格式化时间
   formattedTime := currentTime.Format("2006-01-02 15:04:05")
   fmt.Println("格式化后的时间:", formattedTime)

   // 解析时间
   parsedTime, err := time.Parse("2006-01-02 15:04:05", "2023-08-23 12:30:45")
   if err != nil {
      fmt.Println("时间解析错误:", err)
      return
   }
   fmt.Println("解析后的时间:", parsedTime)

   // 时间增加和减少
   oneHourLater := parsedTime.Add(time.Hour)
   oneHourEarlier := parsedTime.Add(-time.Hour)
   fmt.Println("一小时后:", oneHourLater)
   fmt.Println("一小时前:", oneHourEarlier)

   // 计算时间间隔
   duration := oneHourLater.Sub(parsedTime)
   fmt.Println("时间间隔:", duration)

   // 比较时间
   isAfter := oneHourLater.After(parsedTime)
   isBefore := oneHourLater.Before(parsedTime)
   isEqual := oneHourLater.Equal(parsedTime)
   fmt.Println("是否在之后:", isAfter)
   fmt.Println("是否在之前:", isBefore)
   fmt.Println("是否相等:", isEqual)
}

运行结果如图所示

屏幕截图 2023-08-23 155748.png

2.3.17 数字解析

可以使用strconv包来进行数字的解析和转换。strconv包提供了一系列的函数,用于在字符串和基本数据类型(如intfloat64等)之间进行相互转换。

package main

import (
   "fmt"
   "strconv"
)

func main() {
   // 字符串转整数
   numStr := "12345"
   num, err := strconv.Atoi(numStr)
   if err != nil {
      fmt.Println("字符串转整数出错:", err)
      return
   }
   fmt.Println("字符串转整数:", num)

   // 整数转字符串
   num += 1
   numStr = strconv.Itoa(num)
   fmt.Println("整数转字符串:", numStr)

   // 字符串转浮点数
   floatStr := "3.14"
   float, err := strconv.ParseFloat(floatStr, 64)
   if err != nil {
      fmt.Println("字符串转浮点数出错:", err)
      return
   }
   fmt.Println("字符串转浮点数:", float)

   // 浮点数转字符串
   float += 1.1
   floatStr = strconv.FormatFloat(float, 'f', 2, 64)
   fmt.Println("浮点数转字符串:", floatStr)

   // 字符串转布尔值
   boolStr := "true"
   boolean, err := strconv.ParseBool(boolStr)
   if err != nil {
      fmt.Println("字符串转布尔值出错:", err)
      return
   }
   fmt.Println("字符串转布尔值:", boolean)

   // 布尔值转字符串
   boolean = !boolean
   boolStr = strconv.FormatBool(boolean)
   fmt.Println("布尔值转字符串:", boolStr)
}

2.3.18 进程信息

可以使用osos/exec包以及一些系统相关的包来获取当前进程的信息。

package main

import (
   "fmt"
   "os"
   "os/exec"
   "time"
)

func main() {
   // 当前进程ID
   pid := os.Getpid()
   fmt.Println("当前进程ID:", pid)

   // 父进程ID
   ppid := os.Getppid()
   fmt.Println("父进程ID:", ppid)

   // 进程的启动时间
   creationTime, err := getProcessCreationTime(pid)
   if err != nil {
      fmt.Println("获取进程启动时间失败:", err)
   } else {
      fmt.Println("进程启动时间:", creationTime)
   }

   // 进程的工作目录
   wd, err := os.Getwd()
   if err != nil {
      fmt.Println("获取工作目录失败:", err)
   } else {
      fmt.Println("工作目录:", wd)
   }
}

// 获取进程启动时间
func getProcessCreationTime(pid int) (time.Time, error) {
   cmd := exec.Command("ps", "-o", "lstart", "-p", fmt.Sprint(pid))
   output, err := cmd.Output()
   if err != nil {
      return time.Time{}, err
   }
   // 示例输出格式为:"Mon Jan 2 15:04:05 2006"
   layout := "Mon Jan 2 15:04:05 2006"
   startTime, err := time.Parse(layout, string(output))
   if err != nil {
      return time.Time{}, err
   }
   return startTime, nil
}

首先使用os.Getpid()函数获取当前进程的ID,使用os.Getppid()函数获取父进程的ID。

为了获取进程的启动时间,我们使用了exec.Command()函数创建了一个ps命令的新进程,并指定了一些参数,包括-o lstart表示显示进程的启动时间,-p pid表示指定要查询的进程ID。然后,我们使用cmd.Output()函数获取命令的输出结果。

对于进程的工作目录,我们使用os.Getwd()函数获取当前进程的工作目录。

需要注意的是,这些方法在不同的操作系统上的表现可能会有所不同,请根据实际情况进行调整和处理。另外,还可以使用其他相关的系统信息库来获取更多的进程信息,例如github.com/shirou/gopsutil/process等。