go学习:特殊之处、语言特性

文章目录

  • 背景
  • 特殊之处
    • 闭包
    • vscode
    • defer
    • 错误处理机制
    • for-range遍历
    • 数组和切片
      • 切片的数据结构
    • 字符串和切片
    • map slice
    • 如何对map的key按顺序遍历?
    • 没有面向对象
    • 结构体
      • 没构造函数
    • 方法
    • 面向对象-继承
    • 接口设计
    • 类型断言
    • 测试
    • goroutine
      • 查看是否存在并发
      • 加锁+sleep的蠢方法
      • channel
      • select
      • 某一个goroutine panic了,怎样别影响别的goroutine?
    • 空接口
    • 反射

背景

学习一门新语言,其实大多数东西都是类似的,我们只需要关注这么语言特殊的地方。本文就是在学习过程中记录go特殊的地方

特殊之处

闭包

b站
git_book

vscode

搭建环境

defer

  • 把语句放到栈中,函数结束的时候再执行
  • 一般用于打开文件/数据库后立马defer 关闭,有点像把finally提前
package main

import (
	_ "encoding/json"
	"fmt"
)

func sum(n1 int, n2 int) int {
	defer fmt.Println("ok1 n1 = ", n1)
	defer fmt.Println("ok2 n2 = ", n2)

	n1++
	n2++
	res := n1 + n2

	fmt.Println("ok3 res=", res)
	return res

}

func main() {
	res := sum(1, 2)
	fmt.Println("Res=", res)
}

错误处理机制

不支持try…catch…finlly
使用 defer panic recover

package main

import (
	"errors"
	"fmt"
)

func test() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("err = ", err)
		}
	}()

	num1 := 10
	num2 := 0
	res := num1 / num2
	fmt.Println("Res=", res)
}

func readConf(name string) (err error) {
	if name == "config.ini" {
		return nil
	} else {
		return errors.New("读取文件error")
	}
}

func test02() {
	err := readConf("config.iniaaa")
	if err != nil {
		panic(err)
	}
	fmt.Println("test02ok")
}

func main() {
	test()
	fmt.Println("成功执行test")
	test02()
	fmt.Println("成功执行test02")
	test02()
}

for-range遍历

数组和切片

这个是数组

func test(arr [3]int) {
}

这个是切片

func test(arr []int) {
}

传入函数中之后是值拷贝,函数内修改的数组,是深拷贝后的数组,不会影响函数外的数组。

如果需要函数内部修改,需要传入引用。

fucn test(arr *[3]int) {
	(*arr)[0] = 1
}

长度是数组的一部分,传递参数的时候需要考虑数组的长度

切片的数据结构

可以理解为是一个结构体,保存了首地址、len、cap

如果初始化从一个数组中来,由于使用的是同一个地址,数组和切片的修改都是指向同一个地方,所以会相互影响。

func test03() {
	var arr [5]int = [...]int{1, 2, 3, 4, 5}
	slice := arr[1:4]
	fmt.Println("arr", arr)
	fmt.Println("slice", slice)
	arr[2] = 30
	slice[0] = 20
	fmt.Println("arr", arr)
	fmt.Println("slice", slice)
}

字符串和切片

字符串可以转为[]byte,这是按照byte切分的
中文字符可以转为[]rune,这是按照字符切分的

func testStr() {
	str := "hello@123"
	slice := str[3:]
	fmt.Println("slice=", slice)

	arr2 := []byte(str)
	arr2[1] = 'z'
	str = string(arr2)
	fmt.Println("str=", str)

	arr1 := []rune(str)
	arr1[0] = '啊'
	str = string(arr1)
	fmt.Println("str=", str)
}

map slice

slice中每个元素都是一个map

func testMap() {
	var monsters []map[string]string
	monsters = make([]map[string]string, 2)
	monsters[0] = make(map[string]string, 2)
	monsters[0]["name"] = "牛魔王"
	monsters[0]["age"] = "500"
	fmt.Println(monsters)
}

如何对map的key按顺序遍历?

取出map的key作为slice,slice排序、根据排好序的key取值

func testOrderMap() {
	map1 := make(map[int]int, 10)
	map1[10] = 100
	map1[1] = 13
	map1[4] = 56
	map1[8] = 99

	fmt.Println(map1)

	var keys []int
	for k := range map1 {
		keys = append(keys, k)
	}
	sort.Ints(keys)
	fmt.Println(keys)
}

没有面向对象

但是可以通过接口和结构体开搞

结构体

完全相同的结构体(顺序、变量名、类型)可以相互强转,相当于取别名

可以加上tag,然后根据反射机制获取,常见使用场景:序列化和反序列化

type Stu struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func testStruct() {
	students := make(map[string]Stu)
	stu1 := Stu{"tom", 10}
	stu2 := Stu{"marry", 20}
	students["no1"] = stu1
	students["no2"] = stu2
	fmt.Println(students)

	jsonStr, err := json.Marshal(stu1)
	if err != nil {
		fmt.Println("json 处理出错")
	}
	fmt.Println("jsonStr", string(jsonStr))
}

没构造函数

需要用工厂模式:返回一个包里的私有的结构体:

type privateStudent struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func NewStudent(name string, age int) *privateStudent {
	return &privateStudent{
		Name: name,
		Age:  age,
	}
}

方法

方法是绑定在类型type上的,而不仅仅是struct

如int float都可以有方法

如果一个类型实现了String()方法,fmt.Println() 会调用这个String()方法

面向对象-继承

嵌套匿名结构体

编译器会先找当前,然后找匿名的嵌套结构体

接口设计

有个比较好理解的例子来说明接口的用法

如何实现结构体的排序?
1、定义一个struct Stu
2、再定义一个struct列表,Students
3、实现Sort接口所需的三个方法:Len,Less,Swap

type Stu struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

type StudentList []Stu

func (sl StudentList) Len() int {
	return len(sl)
}
func (sl StudentList) Less(i, j int) bool {
	return sl[i].Age < sl[j].Age
}

func (sl StudentList) Swap(i, j int) {
	sl[i], sl[j] = sl[j], sl[i]
}

// https://www.bilibili.com/e748289c-09fd-4314-80ab-13e3003c0656
func testSortStruct() {
	var students StudentList

	students = append(students, Stu{"tom", 20})
	students = append(students, Stu{"marry", 10})
	students = append(students, Stu{"john", 30})
	fmt.Println(students)

	sort.Sort(students)
	fmt.Println(students)
}

以上便是实现struct自定义排序所需做的

这里面就涉及了接口的设计。我们从Sort源码的角度出发,源码中规定,只要实现了Interface接口,就可以实现对自定义struct的排序,方便了调用者的使用。

go学习:特殊之处、语言特性_第1张图片
具体到我们的例子里,我们的StudentList就是一个自定义的type,这个type实现了Interface的三个方法,于是,他就可以传进去Sort(data Interface)中进行排序了

go学习:特殊之处、语言特性_第2张图片
我们可以想想,如果是冒泡排序的话,是不是总体上就是需要抽象的几个操作实际上就是Len、Less、Swap。

上述就是接口的作用。

类型断言

判断某个变量是啥类型

https://www.bilibili.com/7725bd8f-98c5-4214-b964-01f13e95b058

空接口可以接受任何类型。

最佳实践:判断传入参数类型:

func TypeJudge(items ...interface{}) {
	// 入参是任意个数的任意类型的参数
	for i, x := range items {
		switch x.(type) {
		case bool:
			fmt.Printf("param #%d is bool, val = %v\n", i, x)
		case float64:
			fmt.Printf("param #%d is float64, val = %v\n", i, x)
		case int, int64:
			fmt.Printf("param #%d is int, val = %v\n", i, x)
		case nil:
			fmt.Printf("param #%d is nil, val = %v\n", i, x)
		case string:
			fmt.Printf("param #%d is string, val = %v\n", i, x)
		default:
			fmt.Printf("param #%d is unknown, val = %v\n", i, x)
		}
	}
}

测试

测试框架:
原来的功能函数:

package main

func addUpper(n int) int {
	res := 0
	for i := 1; i < n; i++ {
		res += i
	}
	return res
}

配套的测试函数 xxx_test.go

package main

import (
	"testing"
)

func TestAddUpper(t *testing.T) {
	res := addUpper(10)
	if res != 55 {
		t.Fatalf("testAddUpper failed, should be 55, got %v\n", res)
	} else {
		t.Logf("testAddUpper ok")
	}
}

命令行运行 go test -v

goroutine

查看是否存在并发

go build -race xxx.go

加锁+sleep的蠢方法

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	myMap = make(map[int]int, 10)
	lock  sync.Mutex
)

func test(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res += i
	}
	lock.Lock()
	myMap[n] = res
	lock.Unlock()
}

func main() {
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	time.Sleep(time.Second)
	fmt.Println(myMap)
}

channel

本质是一个队列

使用for range遍历

如果管道不关闭,遍历取出的时候,最后会有deadlock错误。如果管道关闭了,则不会有这个问题

func writeData(intChan chan int) {
	for i := 1; i <= 50; i++ {
		fmt.Printf("write data: %v\n", i)
		intChan <- i
	}
	// 写完了close
	close(intChan)
}

func readData(intChan chan int, exitChan chan bool) {
	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		fmt.Printf("read data: %v\n", v)
	}
	// 读完了close
	close(exitChan)
}

func main() {
	intChan := make(chan int, 50)
	exitChan := make(chan bool, 1)

	go writeData(intChan)
	go readData(intChan, exitChan)

	for {
		_, ok := <-exitChan
		if !ok {
			// close了exitChan说明读完了
			break
		}
	}
}

再来看一个协程算素数的例子:

package main

import (
	"fmt"
	"sort"
	"time"
)

func putNum(intChan chan int) {
	for i := 2; i <= 8000; i++ {
		intChan <- i
	}
	close(intChan)
}

func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	var flag bool
	for {
		num, ok := <-intChan
		if !ok {
			break
		}
		flag = true
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}
		if flag {
			primeChan <- num
		}
	}
	fmt.Println("one goroutine end")
	exitChan <- true
}

func main() {
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 2000)
	exitChan := make(chan bool, 4)

	// 生产者
	go putNum(intChan)
	// 消费者
	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}
	for {
		if len(exitChan) == 4 {
			break
		}
		time.Sleep(time.Second)
	}
	close(primeChan)
	close(exitChan)

	nums := make([]int, 0)
	for num := range primeChan {
		nums = append(nums, num)
	}
	sort.Ints(nums)
	fmt.Println("intChan: ", intChan)
	fmt.Println("finish: ", nums)
}

todo:这个原理是啥?

select

package main

import (
	"fmt"
	"time"
)

func main() {
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}

	time.Sleep(time.Second * 5)
	readNothing := false
	for {
		if readNothing {
			break
		}
		select {
		case v := <-intChan:
			// 如果管道没关闭,但又取不到,自动会到下一个case
			fmt.Printf("read intChan:%d \n", v)
			break
		case v := <-stringChan:
			fmt.Printf("read stringChan:%s \n", v)
			break
		default:
			fmt.Println("read nothing")
			readNothing = true
		}
	}
}

我们会发现,尽管sleep了一会,取的时候还是会有取不到intChan的时候,这是为啥呢?我们来看看select的原理,这个后面可以细读。

某一个goroutine panic了,怎样别影响别的goroutine?

使用defer+recover

package main

import (
	"fmt"
	"time"
)

func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello world")
	}
}

func test() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("test() error", err)
		}
	}()
	var myMap map[int]string
	myMap[0] = "golang"
}

func main() {
	go sayHello()
	go test()

	for i := 0; i < 10; i++ {
		fmt.Println("main() ok= ", i)
		time.Sleep(time.Second)
	}
}

空接口

类似于Python的object,一切都是空接口

反射

啥地方用了反射?

  • 结构体序列化反序列化
  • 适配器
  • go框架

反射可以做什么

  • 获取变量的类型(type)、类别(kind)
  • 获取结构体的信息(结构体的字段、方法)
  • 修改变量值

你可能感兴趣的:(go,go,学习,入门)