Go 语法小结(2)😘 | 青训营

69 阅读14分钟

Collection

主要内容

  • 字符串
  • 数组 & 切片
  • 集合
  • 映射表

字符串

概述

  1. 编码方式:Go语言中的字符串采用UTF-8编码,这是一种可变长度的Unicode编码,支持多种语言字符集。

  2. 通过下标索引:字符串是不可变的,但可以通过下标索引访问单个字符。使用索引访问字符串时,返回的是对应位置的字节值,而不是字符本身。

  3. 构造方式:可以使用双引号 " 或反引号 ` 来构造字符串。双引号字符串支持转义字符,而反引号字符串则是原始字符串,可以包含换行符和特殊字符。

  4. 存储方式:字符串在内存中以字节数组的形式存储,每个字节表示一个字符的编码。

  5. 转为字节数组和字符数组:可以通过类型转换将字符串转换为字节数组或字符数组。字节数组使用[]byte类型表示,而字符数组使用[]rune类型表示。

  6. 不可变性:字符串是不可变的,即一旦创建,就不能修改其内容。如果需要修改字符串,可以将其转换为可变的字节数组或字符数组进行修改。

需要注意的是,由于字符串是不可变的,每次对字符串进行修改时,都会创建一个新的字符串。这意味着在处理大量字符串时,频繁的字符串拼接和修改可能会导致性能问题。在这种情况下,可以使用strings.Builder类型或bytes.Buffer类型来高效地构建和修改字符串。

常用库函数

  1. len():用于获取字符串的长度(字节数)。

    str := "Hello, Go!"
    length := len(str) // 返回12
    
  2. strings.ToLower()strings.ToUpper():用于将字符串转换为小写或大写。

    str := "Hello, Go!"
    lower := strings.ToLower(str) // 返回"hello, go!"
    upper := strings.ToUpper(str) // 返回"HELLO, GO!"
    
  3. strings.TrimSpace():用于去除字符串开头和结尾的空白字符。

    str := "   Hello, Go!   "
    trimmed := strings.TrimSpace(str) // 返回"Hello, Go!"
    
  4. strings.Replace():用于替换字符串中的指定子串。

    str := "Hello, Go!"
    replaced := strings.Replace(str, "Go", "World", -1) // 返回"Hello, World!"
    
  5. 转换为字节数组和字符数组的示例:

    str := "Hello, Go!"
    byteArr := []byte(str)
    fmt.Println(byteArr) // [72 101 108 108 111 44 32 71 111 33]
    
    runeArr := []rune(str)
    fmt.Println(runeArr) // [72 101 108 108 111 44 32 71 111 33]
    
  6. 字符串正则匹配:

    • regexp.MatchString(pattern, s):使用正则表达式 pattern 对字符串 s 进行匹配,返回是否匹配成功。
    • regexp.MustCompile(pattern):编译正则表达式 pattern,返回一个正则表达式对象。
    • re.FindString(s):在字符串 s 中查找第一个匹配正则表达式的子串,并返回该子串。
  7. 字符串查询:

    • strings.Contains(s, substr):判断字符串 s 是否包含子串 substr
    • strings.Index(s, substr):返回子串 substr 在字符串 s 中第一次出现的索引位置。
    • strings.LastIndex(s, substr):返回子串 substr 在字符串 s 中最后一次出现的索引位置。
  8. 字符串截取:

    • s[start:end]:截取字符串 s 中从索引 start 到索引 end-1 的子串。
    • strings.Split(s, sep):将字符串 s 按照分隔符 sep 进行分割,返回一个字符串切片。
    • strings.Join(strs, sep):将字符串切片 strs 中的所有元素使用分隔符 sep 进行连接,返回一个新的字符串。
  9. 字符串拼接:

    • + 运算符:使用 + 运算符可以将两个字符串拼接在一起。
    • fmt.Sprintf(format, a, b, ...):使用格式化字符串和参数将多个字符串拼接在一起,并返回拼接后的结果。
  10. 字符串比较:

    • ==!= 运算符:使用 == 运算符可以比较两个字符串是否相等,使用 != 运算符可以比较两个字符串是否不相等。
    • strings.Compare(a, b):按字典序比较字符串 a 和字符串 b 的大小关系。返回一个整数,如果 a 小于 b,则返回负数;如果 a 等于 b,则返回0;如果 a 大于 b,则返回正数。

这些函数提供了对字符串进行正则匹配、查询和截取的常用操作。根据具体的需求,您可以使用这些函数来处理和操作字符串数据。请注意,某些函数可能需要导入相应的包(如 regexpstrings)才能使用。

与其他类型转换

  • strconv 包中定义了一系列基本类型与字符串相互转化的函数

  • 其他类型可以通过实现 Stringer 接口来定义自定义的字符串表示形式,类似于Java中的 toString() 方法。 Stringer 接口只有一个方法 String(),该方法返回一个字符串表示该类型的值。

数组

Go语言中的数组是一种固定长度的数据结构,用于存储具有相同类型的元素。

  1. 定义数组:

    var arr [5]int // 定义一个长度为5的整数数组
    
  2. 初始化数组:

    • 使用索引逐个赋值:
    • 使用初始化列表:
      arr := [5]int{10, 20, 30, 40, 50}
      
  3. 遍历数组:

    • 使用for循环和索引:
      for i := 0; i < len(arr); i++ {
          fmt.Println(arr[i])
      }
      
    • 使用for range循环:
      for index, value := range arr {
          fmt.Println(index, value)
      }
      
  4. 转换到切片

 slice := arr[:] //全部转成对应切片
 slice := arr[1:10] //部分转成对应切片

切片

当涉及到动态长度的数据集合时,切片(Slice)是Go语言中常用的数据类型。切片是对数组的一个引用,它提供了动态增长和灵活操作的能力。以下是关于切片的定义、遍历和追加等常用操作:

  1. 定义切片:

    var slice []int // 定义一个整数切片
    
  2. 初始化切片:

    • 使用切片字面量:
      slice := []int{10, 20, 30, 40, 50}
      
    • 使用make()函数:
      slice := make([]int, 5) // 创建一个长度为5的整数切片,初始值为0
      
  3. 遍历切片(和数组一致)

  4. 追加元素:

    slice = append(slice, 60) // 追加一个元素到切片末尾
    
  5. 复制切片

src := []int{1, 2, 3}
dest := make([]int, len(src))
copy(dest, src) // 将 src 切片的元素复制到 dest 切片

集合

使用数组(切片) 实现栈的功能

type Stack struct {
	arr    []int
	length int
}

func New() Stack {
	return Stack{
		arr:    make([]int, 0),
		length: 0,
	}
}

func (s *Stack) Push(a int) {
	s.arr = append(s.arr, a)
	s.length += 1
}

func (s *Stack) Empty() bool {
	return s.length <= 0
}

func (s *Stack) Pop() int {
	if s.Empty() {
		panic("栈为空")
	}
	s.length--
	return s.arr[s.length]
}

func (s *Stack) Top() int {
	if s.Empty() {
		panic("栈为空")
	}
	return s.arr[s.length-1]
}

使用 container 包中对应接口

  1. heap:堆,可以定义比较、交换、弹出、插入规则实现排序。

需要手动实现标准库中的 heap 相关接口才能使用

/** 标准库接口
// container/heap
type Interface interface {
	sort.Interface
	Push(x any) // add x as element Len()
	Pop() any   // remove and return element Len() - 1.
}

// sort
type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}
*/
// 自定义小顶堆
type MinHeap struct {
	arr    []any
	length int
}

func New() MinHeap {
	return MinHeap{
		arr:    make([]any, 0),
		length: 0,
	}
}

func (h *MinHeap) Push(x any) {
	h.arr = append(h.arr, x)
	h.length++
}

func (h *MinHeap) Pop() any {
	h.length--
	return h.arr[h.length]
}

func (h *MinHeap) Len() int {
	return h.length
}

func (h *MinHeap) Less(i, j int) bool {
	return h.arr[i].(int) < h.arr[j].(int)
}

func (h *MinHeap) Swap(i, j int) {
	tmp := h.arr[i]
	h.arr[i] = h.arr[j]
	h.arr[j] = tmp
}
// arr := []int{9, 5, 7, 41, 8, 1, 87, 4, -14, -1, 5, 61, 81, 11, 541}
func TestHeap(data []int) {
	h := New()
	heap.Init(&h)
	for _, v := range data {
		heap.Push(&h, v)
	}
	for h.Len() > 0 {
		tmp := heap.Pop(&h)
		var num = tmp.(int)
		fmt.Printf("%d ", num)
	}
	// -14 -1 1 4 5 5 7 8 9 11 41 61 81 87 541
}
  1. list:双向链表,提供插入,移动,删除等功能。可以作为栈或队列使用

可以直接使用

func TestList(data []int) {
	var l = list.New()
	for _, v := range data {
	    l.PushBack(v)
	}
	for l.Len() > 0 {
	    tmp := l.Front()
	    var num = tmp.Value.(int)
	    fmt.Printf("%d ", num)
	    l.Remove(tmp)
	}
}
  1. ring:环形链表,可以在常量时间内进行插入、删除和移动操作。

可以直接使用

func TestRing(data []int) {
	b := len(data)
	r := ring.New(b)
	for _, v := range data {
		r.Value = v
		r = r.Next()
	}
	for i := 0; i < 2*b; i++ {
		tmp := r.Value.(int)
		fmt.Printf("%d ", tmp)
		r = r.Next()
	}
}

映射表

在Go语言中,映射(Map)是一种键值对的集合,用于存储和检索数据。以下是映射的定义和常用操作函数:

  1. 定义映射:

    var m map[keyType]valueType // 定义一个映射,keyType为键的类型,valueType为值的类型
    
  2. 初始化映射:

    • 使用映射字面量:
      m := map[keyType]valueType{
          key1: value1,
          key2: value2,
          // ...
      }
      
    • 使用make()函数:
      m := make(map[keyType]valueType) // 创建一个空的映射
      
  3. 插入或更新键值对:

    m[key] = value // 插入或更新键为key的值为value
    
  4. 删除键值对:

    delete(m, key) // 删除键为key的键值对
    
  5. 获取键对应的值:

    value := m[key] // 获取键为key的值
    
  6. 检查键是否存在:

    value, ok := m[key] // 检查键为key的值是否存在,如果存在,ok为true,否则为false
    
  7. 遍历映射:

    for key, value := range m {
        // 处理键值对
    }
    

这些操作函数提供了对映射进行插入、删除、获取和遍历等常用操作的能力。映射是一种非常有用的数据结构,可以用于存储和检索键值对数据。需要注意的是,映射中的键是唯一的,每个键只能对应一个值。


Advanced golang

主要内容

  • OOP
  • Goroutine 与并发
  • 反射
  • 更多标准库

OOP

针对Go语言面向对象的一些语法特性进行补充

DIP

面向对象设计中的依赖倒置原则(Dependency Inversion Principle,DIP)。 是 SOLID 原则中的一条,提出了以下设计指导原则:

  1. 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
  2. 抽象不应该依赖于具体实现细节。具体实现细节应该依赖于抽象。

这个原则的核心思想是,模块之间的依赖关系应该通过抽象来建立,而不是通过具体的实现类。这样可以降低模块之间的耦合性,提高代码的灵活性和可维护性。

在Golang 中导包不允许循环依赖,因此设计接口和实现类时,需要遵守DIP原则。

指针接收器和对象接收器

  • 在 Go 语言中,方法可以定义在类型上,可以使用指针接收器(pointer receiver)或对象接收器(value receiver)。 指针接收器和对象接收器的主要区别在于方法对接收器的处理方式。

    1. 指针接收器:

      • 使用指针作为方法的接收器。
      • 方法可以修改接收器指向的对象的状态。
      • 方法在调用时会直接操作原始对象,而不是对象的副本。
      • 适用于需要修改对象状态或避免复制大对象的情况。
    2. 对象接收器:

      • 使用对象作为方法的接收器。
      • 方法不能修改接收器指向的对象的状态。
      • 方法在调用时会操作接收器的副本,而不是原始对象。
      • 适用于不需要修改对象状态的情况。

以下是一个示例,展示了指针接收器和对象接收器的区别:

type Rectangle struct {
	width  int
	height int
}

// 指针接收器的方法, 可以修改原始对象的属性
func (r *Rectangle) SetWidthPointer(newWidth int) {
	r.width = newWidth
}

// 对象接收器的方法, 不能修改原始对象的属性
func (r Rectangle) SetWidthValue(newWidth int) {
	r.width = newWidth
}

类型转换和多态

  • 在实现接口时,不同的接收器会直接影响能否进行基于接口的多态。继续之前的例子:
type Shape interface{
   SetWidth(int)
}

type Rectangle struct {
	width  int
	height int
}

func testConvert1(){
	var shape Shape
	r := Rectangle{}
	shape = r
	shape.SetWidth(10)
}

func testConvert2(){
	var shape Shape
	r := &Rectangle{}
	shape = r
	shape.SetWidth(10)
}

使用指针接收器的实现方法

func (r *Rectangle) SetWidth(newWidth int) {
	r.width = newWidth
}
  • testConvert1 编译报错,testConvert2 不报错
  • 原因是: *Rectangle 和 Rectangle 被认为是不同的类型。*Rectangle 实现了接口,但是Rectangle没有实现接口。

使用对象接收器的方法

func (r Rectangle) SetWidth(newWidth int) {
	r.width = newWidth
}
  • testConvert1 不报错,testConvert2 不报错
  • 原因是: Golang 有语法糖自动处理对象到引用的转换。若 Rectangle实现了接口,则 *Rectangle也自动实现了接口;反之不对

类型嵌入和阴影现象

  • 当一个类型(结构体或接口)嵌入了另一个类型时,它会继承嵌入类型的字段和方法,但是不能重写结构体的方法。也不存在父类子类的继承关系。

  • 在 Go 语言中,阴影方法(Shadowed Method)指的是在嵌入类型中定义了与外部类型相同名称的方法,从而隐藏了外部类型的同名方法。 当调用阴影方法时,实际上调用的是嵌入类型中定义的方法,而不是外部类型中的同名方法。这种情况下,外部类型的同名方法被“阴影”,无法直接访问。

package main

import "fmt"

type Person struct {
	Name string
}

func (p Person) Greet() {
	fmt.Println("Hello, I'm a person.")
}

type Employee struct {
	Person
}

func (e Employee) Greet() {
	fmt.Println("Hello, I'm an employee.")
}

func main() {
	e := Employee{}
	e.Greet() // 输出:Hello, I'm an employee.
}

在上述示例中,我们定义了一个 Person 结构体和一个 Employee 结构体,Employee 结构体嵌入了 Person 结构体。

两个结构体都定义了名为 Greet 的方法。当我们创建一个 Employee 对象并调用 Greet 方法时,实际上调用的是 Employee 结构体中定义的 Greet 方法,而不是 Person 结构体中的同名方法。这是因为 Employee 结构体中的方法“阴影”了 Person 结构体中的同名方法。

需要注意的是,如果我们想要在 Employee 结构体中调用 Person 结构体中的方法,可以通过显式调用 e.Person.Greet() 来实现。

Goroutine

概念

Goroutine 是 Go语言中的一种轻量级线程,用于并发执行代码。Goroutine 是由 Go 运行时(runtime)管理的,可以在单个线程上运行成千上万个 Goroutine,实现高效的并发编程。

以下是 Goroutine 的一些特点和使用方式:

  1. 轻量级:Goroutine 的创建和销毁开销很小,每个 Goroutine 默认只占用几 KB 的内存。

  2. 通信:Goroutine 之间可以通过通道(channel)进行通信,实现数据的同步和共享。

Goroutine 的使用场景包括但不限于以下情况:

  1. 并发任务:当需要同时执行多个任务时,可以使用 Goroutine 实现并发执行,提高程序的性能和响应能力。

  2. 非阻塞的异步操作:当需要执行非阻塞的异步操作时,可以使用 Goroutine 来处理并发的异步任务。

  3. 事件驱动编程:在事件驱动的编程模型中,可以使用 Goroutine 来处理事件的并发处理和响应。

  4. 并行计算:当需要进行并行计算时,可以使用 Goroutine 实现任务的并行执行,充分利用多核处理器的性能。

需要注意的是,由于 Goroutine 是由 Go 运行时调度的,因此不能直接控制 Goroutine 的执行顺序。如果需要控制 Goroutine 的执行顺序,可以使用通道(channel)或其他同步原语来实现。此外,需要注意在 Goroutine 中的并发访问共享数据

相关关键字

1.chan & go & selectchan 用于声明一个通道; go 用于声明一个匿名函数为 go routine. 通过 select,我们可以同时等待多个通道的消息,并选择首先到达的通道进行处理。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "Hello"
    }()

    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- "World"
    }()

    select {
     case msg1 := <-ch1:
        fmt.Println(msg1)
     case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

Goroutine

  1. Goroutine 之间的数据传递和同步:

    jobs 通道接收任务并将结果发送到 results 通道。在 main 函数中,我们创建了多个 Goroutine 来执行任务,并通过通道进行任务的分发和结果的收集。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func worker(id int, jobs <-chan int, results chan<- int) {
        for j := range jobs {
            fmt.Println("Worker", id, "started job", j)
            time.Sleep(time.Second)
            fmt.Println("Worker", id, "finished job", j)
            results <- j * 2
        }
    }
    
    func main() {
        jobs := make(chan int, 5)
        results := make(chan int, 5)
    
        for w := 1; w <= 3; w++ {
            go worker(w, jobs, results)
        }
    
        for j := 1; j <= 5; j++ {
            jobs <- j
        }
        close(jobs)
    
        for r := 1; r <= 5; r++ {
            <-results
        }
    }
    
  2. 控制并发操作的顺序和同步: 使用通道 ch 来控制两个 Goroutine 的顺序和同步。第一个 Goroutine 等待数据,第二个 Goroutine 发送数据

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        var wg sync.WaitGroup
        wg.Add(2)
    
        ch := make(chan bool)
    
        go func() {
            defer wg.Done()
            fmt.Println("Goroutine 1: Start")
            <-ch
            fmt.Println("Goroutine 1: Done")
        }()
    
        time.Sleep(time.Second)
    
        go func() {
            defer wg.Done()
            fmt.Println("Goroutine 2: Start")
            ch <- true
            time.Sleep(time.Second)
            fmt.Println("Goroutine 2: Done")
        }()
    
        wg.Wait()
    }
    

结果

Goroutine 1: Start
Goroutine 2: Start
Goroutine 1: Done
Goroutine 2: Done

反射

自定义反射

type Person struct {
	Name string `json:"name" validate:"required"`
	Age  int    `json:"age" validate:"gte=0"`
}

func (p Person) GetName() string {
	return p.Name
}

func Run() {
	p := Person{Name: "John Doe", Age: 30}
	// 获取结构体类型
	t := reflect.TypeOf(p)
	
	// 获取方法
	method, b := t.MethodByName("GetName")
	if b {
		println(method.Index)
	}
	
	// 遍历结构体的字段
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag
		// 获取标签的值
		jsonTag := tag.Get("json")
		validateTag := tag.Get("validate")
		fmt.Printf("Field: %s, JSON Tag: %s, Validate Tag: %s\n", field.Name, jsonTag, validateTag)
	}
}

常用标签

在 Go 语言中,常用的标签可以用于相关框架或库,以提供额外的元数据或配置信息。以下是一些常见的标签:

  1. json:用于指定 JSON 序列化和反序列化时的字段名称和选项。

  2. xml:用于指定 XML 序列化和反序列化时的字段名称和选项。

  3. gorm:用于 GORM(Go 语言的 ORM 库)中,用于指定数据库表的名称、字段的约束和选项等。

  4. protobuf:用于 Protocol Buffers(protobuf)中,用于指定字段的序列化和反序列化选项。

  5. validate:用于验证库(如 go-playground/validator)中,用于指定字段的验证规则和选项。

  6. http:用于 HTTP 框架(如 Gin、Echo)中,用于指定路由、中间件和请求处理函数等。

  7. yaml:用于 YAML 序列化和反序列化时的字段名称和选项。

这些标签可以根据具体的框架或库的要求使用,并提供了一种在结构体字段上添加元数据的方式。通过使用这些标签,可以实现自动的序列化、反序列化、验证、路由等功能,提高开发效率和代码的可维护性。

更多标准库

补充这一节及今后可能会涉及的标准库包,包括同步、web相关的包

  1. JSON 相关包(encoding/json):

    • json.Marshal():将 Go 对象转换为 JSON 字符串。
    • json.Unmarshal():将 JSON 字符串解析为 Go 对象。
  2. Socket 相关包(net):

    • net.Dial():建立与远程主机的网络连接。
    • net.Listen():监听指定的网络地址,接受传入的连接请求。
  3. HTTP 相关包(net/http):

    • http.Get():发送 HTTP GET 请求并返回响应。
    • http.Post():发送 HTTP POST 请求并返回响应。
    • http.HandleFunc():注册 HTTP 请求处理函数。
  4. 正则表达式相关包(regexp):

    • regexp.MatchString():检查字符串是否与正则表达式匹配。
    • regexp.FindString():查找字符串中与正则表达式匹配的子串。
  5. 同步互斥相关包(sync):

    • sync.Mutex:互斥锁,用于保护共享资源的并发访问。
    • sync.WaitGroup:等待组,用于等待一组 Goroutine 完成。
  6. 反射相关包(reflect):

    • reflect.TypeOf():获取值的类型信息。
    • reflect.ValueOf():获取值的反射值。
    • reflect.New():创建一个指向类型的新实例。