作者: ethan

  • go – defer延迟函数

    go语言中,defer语句的作用就是不管程序是否出现异常,均在函数退出时自动执行相关代码,常用于函数结束后关闭一些资源的调用,及时的释放资源。

    不过,对于defer往往有两个常见的混淆点或者说误区。

    误区一: 参数值

    看下面一段代码:

    func TestOther(t *testing.T) {
    	i := 5
    
    	defer func() {
    		t.Log(i)
    	}()
    	
    	i = 6
    	return
    }

    运行结果为:

    这是因为,函数执行完成后,执行defer时,defer获取的变量i,就为该函数开始定义的i, 为同一个变量,所以输出的是改变后的i的值。

    再看下面一段代码:

    func TestOther(t *testing.T) {
    	i := 5
    
    	defer func(i int) {
    		t.Log(i)
    	}(i)
    
    	i = 6
    	return
    }

    运行结果为:

    可以看到,此时的运行结果,输出的为 5,这是因为,defer语句的参数值,是在defer语句出现的时候就确定下来的,而这里defer函数出现的时候,传入参数 i 此时的值是5,所以最终输出的结果为5。

    误区二: return & defer

    看下面一段代码:

    func deferFunc(i int) (result int) {
    	defer func() {
    		result++
    	}()
    	return i
    }
    
    func TestOther(t *testing.T) {
    	i := 1
    	result := deferFunc(i)
    	t.Log(result)
    }

    运行结果为:

    这是因为,函数的返回return,并不是原子的,其分为设置返回值,以及返回返回值,而上述代码中的函数 deferFunc 包含defer, 所以其返回结果的过程为: return设置返回值result=1 => defer result++ => 函数返回result , 所以最终输出的结果为 result=2

  • go-超时控制time.After()

    业务逻辑中,对一些逻辑进行合理的超时控制,往往能够极大的提升程序的稳定性,特别是对一些接口的实现上。

    go语言中,通过time.After()可以很优雅的实现超时控制,且不会造成阻塞。

    time.After()底层

    // After waits for the duration to elapse and then sends the current time
    // on the returned channel.
    // It is equivalent to NewTimer(d).C.
    // The underlying Timer is not recovered by the garbage collector
    // until the timer fires. If efficiency is a concern, use NewTimer
    // instead and call Timer.Stop if the timer is no longer needed.
    func After(d Duration) <-chan Time {
    	return NewTimer(d).C
    }

    time.After()表示time.Duration长的时候后返回一条time.Time类型的通道消息,但是在取出channel内容之前不阻塞,后续程序可以继续执行。

    特性样例

    func TestOther(t *testing.T) {
    	t.Log(time.Now().Unix())
    	tm := time.After(3  * time.Second)
    
    	t.Log("waiting...")
    
    	t.Log(<-tm)
    	t.Log(time.Now().Unix())
    }
    

    输出:

    time.After()

    可以看到,调用time.After()后,程序非阻塞,继续执行,知道读取tm管道信息时,阻塞3s后,才读取到管道信息

    超时控制

    func waitForStopOrTimeOut(stopCh <- chan struct{}, timeout time.Duration) <- chan bool {
    	stopWithTimeOut := make(chan bool)
    	go func() {
    		select {
    		case <- stopCh:
    			// 若接收到业务逻辑正常结束的消息,则为自然结束
    			fmt.Println("自然结束")
    			stopWithTimeOut <- false
    		case <- time.After(timeout):
    			// 若timeout时间内,未接收到业务逻辑正常结束的消息,则为超时
    			// timeout时间后,time.After(timeout)可读取到管道信息
    			fmt.Println("超时")
    			stopWithTimeOut <- true
    		}
    		close(stopWithTimeOut)
    	}()
    	return stopWithTimeOut
    }
    
    func doSomething()  {
    	time.Sleep(time.Second * 2)
    }
    
    func TestOther(t *testing.T) {
    	t.Log("start")
    
    	stopCh := make(chan struct{})
    	go func() {
    		// 需要控制超时时间的业务逻辑
    		doSomething()
    
    		// 业务逻辑正常执行完,通知管道stopCh,执行完成
    		stopCh <- struct {}{}
    	}()
    
    	// 启用超时控制
    	stopWithTimeOut := waitForStopOrTimeOut(stopCh, 3 * time.Second)
    
    	select {
    	// 若为超时,正常结束,isTimeOut=false, 否则为true
    	case isTimeOut := <- stopWithTimeOut:
    		if isTimeOut {
    			t.Log("end timeout")
    		} else {
    			t.Log("end ok")
    		}
    	}
    	return
    }

    上述代码,限制的超时时间是3s, waitForStopOrTimeOut(stopCh, 3 * time.Second), doSomething()中,time.Sleep()分别为2s, 4s时的执行结果分别为:

    可见,我们已经实现了符合预期的超时控制逻辑。

  • 全角&半角转换

    简介

    在计算机字符被设计时,西方字符,例如拉丁字母、符号和数字都被归位到了「1字节」所能表示的「256」个空间中。随着计算机向全世界的普及和发展,在东亚地区(中、日、韩),由于这些国家的字符太多,所以必须使用「2个字节」来编码使得能够容纳「65536」个字符。

    简单地讲,全角字符占用2个字节位置,半角字符(Half-width characters)占用1个字节位置。

    原理

    全角字符unicode编码从65281~65374,半角字符unicode编码从33~126,全角空格为12288,半角空格为32,其他字符半角(33-126)与全角(65281-65374)的对应关系是:均相差65248

    实现

    // 全角转半角
    func DbcToSbc(s string) string {
    	var strLst []string
    	for _, i := range s {
    		insideCode := i
    		if insideCode == 12288 {
    			insideCode = 32
    		} else {
    			insideCode -= 65248
    		}
    		if insideCode < 32 || insideCode > 126 {
    			strLst = append(strLst, string(i))
    		} else {
    			strLst = append(strLst, string(insideCode))
    		}
    	}
    	return strings.Join(strLst, "")
    }
  • 中文标点转为英文标点-golang

    业务中,中文标点符号常常是一个问题,在涉及到一些文本相关的逻辑时,为了避免复杂的逻辑,我们一般统一将中文标点转换为英文标点,再进行相关计算。

    示例

    var punctuationMap = map[rune]rune{
    	8216: 39, // '
    	8217: 39, // '
    	8220: 34, // "
    	8221: 34, // "
    	12290: 46, // .
    	12304: 91, // [
    	12305: 93, // ]
    	65281: 33, // !
    	65288: 40, // (
    	65289: 41, // )
    	65292: 44, // ,
    	65306: 58, // :
    	65307: 59, // ;
    	65311: 63, // ?
    }
    func PunctuationToEn(text string) string {
    	text = strings.Map(func(r rune) rune {
    		if v, ok := punctuationMap[r]; ok {
    			return v
    		}
    		return r
    	}, text)
    	return text
    }
    func TestOther(t *testing.T) {
    	text := "中国,还!在。?!,;:“”‘'()【】"
    	t.Log(text)
    	t.Log(PunctuationToEn(text))
    }

    以上是提供的常见的,在展现上中英文符号类似的一些中文标点符号的转换,输出结果为:

    参考

    https://www.cnblogs.com/jjjs/p/4763945.html

    https://blog.csdn.net/lichaobxd/article/details/105773492

  • golang “继承”

    go语言结构体没有继承的概念,但可以通过结构体组合的方式,实现类似于继承的效果。

    如下:

    type Animal struct {
    	Name string
    	Sound string
    }
    
    func (an *Animal) SetSound(sound string)  {
    	an.Sound = sound
    }
    
    type Cat struct {
    	Animal
    }
    
    type Dog struct {
    	a Animal
    }
    
    func TestOther(t *testing.T) {
    	cat := Cat{}
    	cat.Name = "cat"
    	cat.SetSound("miao")
    	t.Log(cat)
    
    	dog := Dog{}
    	dog.a.Name = "dog"
    	dog.a.SetSound("wang")
    	t.Log(dog)
    }

    结果输出:

    可以看到,以上两种操作方式的结果是一致的,但是通过隐式指定的方式,原结构体Animal的字段和方法就像是被“继承”到结构体Cat里一样。

    cat可以直接调用Animal结构体中的Name等字段以及SetSound方法,就像是Cat结构的原生字段和方法一样。

  • map底层详解 – golang

    Go语言的map使用Hash表作为底层实现,一个Hash表中可以有多个bucket, 而每个bucket保存了map中的一组或多组键值对。

    Hash函数

    选择hash函数,主要考察的是两点: 性能、碰撞概率。golang会根据硬件选择hash算法,如果cpu支持aes,则使用aes hash, 否则使用memhash。

    哈希函数会将传入的key值进行哈希运算,得到一个唯一的值。go语言把生成的哈希值一分为二,比如一个key经过哈希函数,生成的哈希值为:8423452987653321,go语言会这它拆分为84234529,和87653321。那么,前半部分就叫做高位哈希值,后半部分就叫做低位哈希值

    底层数据结构

    map的底层数据结构

    type hmap struct {
    	count     int // 当前保存的元素个数
    	flags     uint8 // map状态标识
    	B         uint8  // bucket数组的大小,2^B=len(buckets)。可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子即:map长度=6.5*2^B
    	noverflow uint16 // 溢出桶里bmap大致的数量,B<16时为精确值,否则为近似值;
    	hash0     uint32 // hash 种子
    	buckets    unsafe.Pointer // 指向一个数组(连续内存空间),数组的类型为[]bmap,这个字段我们可以称之为正常桶
    	oldbuckets unsafe.Pointer // 老bucket数组,用于扩容
    	nevacuate  uintptr        // 搬迁进度,小于nevacuate的已经搬迁
    
    	extra *mapextra // 溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
    }

    bucket的底层数据结构

    type bmap struct {
    	tophash [bucketCnt]uint8 // bucketCnt = 8
    }

    bucket的底层数据结构(逻辑底层)

    type bmap struct {
    	tophash [bucketCnt]uint8 // bucketCnt = 8
            data []byte // key value 数据 key0/key1/key2/.../value1/value2/value3...
            overflow *bmap // 溢出bucket的地址
    }

    每个bucket可以存储8个键值对

    • tophash是一个长度为8的整型数组,Hash值相同的键(准确的说是Hash值低位相同的键)存入当前bucket时,会将Hash值的高位存储在该数组中,便于后续查找匹配
    • data区存放的是key-value数据,存放顺序是: key0/key1/key2/…/value1/value2/value3…,如此存放,是为了节省字节对齐带来的空间浪费。(如图所示,蓝色为高位,红色为低位)
    • overflow指针指向下一个bmap,将所有有冲突的键链接起来(由于每个bmap可以存放8个键值对,当超过是,就会申请一个新的bmap(overflow bucket)挂在这个bmap的后面形成链表)
  • golang切片传参

    golang切片传参,实际传的是指针。

    如果函数内,对切片进行扩容操作,则会生成新的切片,导致原切片的值不变,如下:

    func sliceRise(s []int)  {
    	s = append(s, 0)
    	fmt.Printf("%p \n", s)
    	for i := range s {
    		s[i]++
    	}
    }
    
    func TestOther(t *testing.T) {
    	s1 := make([]int, 2, 2)
    	s2 := make([]int, 2, 3)
    	sliceRise(s1)
    	fmt.Printf("addr:%p, val: %v \n", s1, s1)
    	sliceRise(s2)
    	fmt.Printf("addr:%p, val: %v \n", s2, s2)
    }

    我们来看一下,输出的结果,在输出结果前,最好你也想一下,看是不是跟你所预期的一致

    可以看到,s1切片的值未发生变化,s2切片的值发生了变化。同时也可以看到,s1传参后,函数内部切片s与s1的地址不同,而s2传参后,函数内部切片s与s1的地址相同。

    说明

    s1传入函数后,发生了扩容,函数内的s切片已经是一个跟s1无关的新切片了,其值的修改,不会影响到s1的值。

    而s2传入函数后,并没有发生扩容,函数内的s切片跟s2切片共享底层数组,所以,对s切片的修改,也即是对s2切片的修改。

  • golang中文字符处理

    因为中文字符的特殊性,基本上各类语言对于中文字符都需要进行特定的处理。

    原理说明

    golang中,字符串的底层是通过byte数组来实现的, 我们看下byte的底层结构:

    type byte = uint8

    可以看到,byte类型的底层实际为uint8类型

    字符串的底层结构为:

    type StringHeader struct {
    	Data uintptr // 指向底层字节数组的指针
    	Len  int  // 字符串的字节长度
    }

    由于golang默认为UTF-8编码,所以,中文进行存储时,如“你好”,会存储为:

    “你”编码为\xe4\xbd\xa0,“好”编码为\xe5\xa5\xbd

    rune类型底层结构

    type rune = int32

    rune类型是int32的别名(-231~231-1),对于byte(-128~127),可表示的字符更多。由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。

    byte 与 rune对比

    func TestOther(t *testing.T) {
    	text := "abcd1234浮生无事"
    	fmt.Println([]byte(text))
    	fmt.Println([]rune(text))
    }
    byte & rune

    通过上面的例子,我们可以直观的看到,rune将字符串拆分为多个Unicode 字符序列,而byte则将其拆分为字节序列。

    所以,利用rune就可以解决各类中文字符串计算问题。

    中文字符串处理

    func TestOther(t *testing.T) {
    	text := "abcd1234浮生无事"
    	textLen := len(text)
    	t.Log("len:" + strconv.FormatInt(int64(textLen), 10))
    	for i := 0; i <= textLen - 1; i++ {
    		t.Log(fmt.Sprintf("word:%s", text[i:i+1]))
    	}
    }

    可以看到,因为字符串计算是按照字节来计算的,所以,无论是字符串长度,还是字符串切割,都是按照字节来进行,导致中文乱码(注意在golang中一个汉字占3个byte)。

    此时,为解决该类情况,正确的对中文进行处理:

    func TestOther(t *testing.T) {
    	text := "abcd1234浮生无事"
    	textRune := []rune(text)
    	textLen := len(textRune)
    	t.Log("len:" + strconv.FormatInt(int64(textLen), 10))
    	for i := 0; i < textLen; i++ {
    		t.Log(fmt.Sprintf("word:%s", string(textRune[i])))
    	}
    }

    将字符串,转为[]rune类型后,即可以对中文字符进行正确的处理,不会出现乱码。

  • Emoji表情json存储 – golang

    golang中,带有表情的字符串,通过json.Marshal()后,并不会进行unicode,所以会导致存储进数据库中的表情为乱码。

    例子:

    func TestOther(t *testing.T) {
    	str := "哈哈😝👌"
    	strr, _ := json.Marshal(str)
    	t.Log(str)
    	t.Log(fmt.Sprintf("%s", strr))
    }

    输出:

    下面,我们开发了一个将字符串unicode的方法:

    type EmojiUnicodeString string
    
    func (em EmojiUnicodeString) MarshalJSON() ([]byte, error) {
    	encodes := utf16.Encode([]rune(em))
    	ret := `"`
    	for _, enc := range encodes {
    		//ascii可见字符范围0-9a-zA-Z
    		if (enc >= 0x30 && enc <= 0x39) || (enc >= 0x41 && enc <= 0x5A) || (enc >= 0x61 && enc <= 0x7A) {
    			ret = ret + string(rune(enc))
    		} else { //其他都使用utf16编码
    			if encStr := strconv.FormatUint(uint64(enc), 16); encStr != "" {
    				//补齐4位长度
    				diff := 4 - len(encStr)
    				for i := 0; i < diff; i++ {
    					encStr = "0" + encStr
    				}
    				ret = ret + `\u` + encStr
    			}
    		}
    	}
    	ret = ret + `"`
    	return []byte(ret), nil
    }

    测试:

    func TestOther(t *testing.T) {
    	str := "哈哈😝👌"
    	strr, _ := json.Marshal(str)
    	t.Log(str)
    	t.Log(fmt.Sprintf("%s", strr))
    
    	str2 := EmojiUnicodeString(str)
    	strr2, _ := json.Marshal(str2)
    	t.Log(fmt.Sprintf("%s", strr2))
    }

    输出:

    可以看到,该带有表情的字符串已经unicode话,此时就可以直接进行数据库存储了。

  • golang不定参数传参-切片解构

    golang支持不定参数传参,不过有时候,我们并不能很直观的知道需要传哪些参数,这些参数是通过其他的一些逻辑得到的,如以下一个go-redis封装方法示例:

    func (r RedisClient) HMSet(key string, values ...interface{}) error {
    	return r.Client.HMSet(r.Ctx, key, values ...).Err()
    }

    可以看到,如果我们想要批量设置filed, value时,因为field,value是一组组的值,且并不是直观的可以直接写入参数,此时我们就需要用到 切片打散 的方式,如下:

    	var fieldValue []interface{}
    	for _, word := range param.Words {
    		fieldValue = append(fieldValue, strconv.FormatInt(word.WordId, 10), word.Word)
    	}
    	err = redisClient.HMSet(key, fieldValue...)

    通过 fieldValue… 的方式,即可以将该切片打散,进行不定传参。

    同时,该方式也可用于两数组合并,如:

    	var arr1 = []string{
    		"a",
    		"b",
    		"c",
    	}
    	var arr2 = []string{
    		"d",
    		"e",
    	}
    	arr1 = append(arr1,arr2...) // arr2的元素被打散一个个append进arr1