├── .gitignore ├── CNAME ├── LICENSE ├── README.md ├── _config.yml ├── algorithm ├── 3sum-closest.go ├── add-binary.go ├── array-intersection.go ├── docs │ ├── 3sum-closest.md │ ├── array-intersection.md │ ├── bubble-sort.md │ ├── insertion-sort.md │ ├── longest-common-prefix.md │ ├── match-sunday-string.md │ ├── reverse-string.md │ ├── search-first-unique-char.md │ ├── selection-sort.md │ ├── sliding-window-maximum.md │ └── verify-palindrome.md ├── first-unique-char.go ├── ip-start-end-query.go ├── kmp-string.go ├── largest-subsequence.go ├── line.go ├── longest-common-prefix.go ├── longest-substring-without-repeating-characters.go ├── match-sunday-string.go ├── multi-array-sort.go ├── removeNthFromEnd.go ├── reverse-string.go ├── reverse_list.go ├── sliding-window-maximum.go ├── sort-colors.go ├── sort │ ├── bubble_sort.go │ ├── insertion_sort.go │ └── selection_sort.go ├── testdata │ └── ip.data ├── upper_bound.go └── verify-palindrome.go ├── base ├── go-gpm.md ├── go-grammar.md ├── go-scheduler-base.md ├── go-scheduler.md ├── redis-data-structure.md ├── redis-rdb.md └── redis.md ├── images ├── 1.458698c8.jpg ├── 1.81971505.jpg ├── 1.f441ca2d.jpg ├── 1.fc3a2c47.jpg ├── 11b30c167de447ffad6d1ab741f921f6.jpeg ├── 1289934-20190621232209365-1000366002.png ├── 1289934-20190621233618769-504231907.png ├── 1289934-20190622000959260-539243592.png ├── 1289934-20190622001013515-677922001.png ├── 16530eac181d94c8.jpg ├── 16530eac182b1b66.jpg ├── 16530eac18882d66.jpg ├── 16c4d9dd1b8235c3.jpg ├── 16dddb3da13ee747.jpg ├── 16dddce6e7f823b4.jpg ├── 16ddddda50800f63.jpg ├── 16dde266675798b5.jpg ├── 16dde33e7e6444f3.jpg ├── 16dde36171697ce0.jpg ├── 16dde38e3d6c258f.jpg ├── 16dde41e63808dc7.jpg ├── 17188139512ef9a8.jpg ├── 17188139b47a27ca.jpg ├── 17188139f32b2558.jpg ├── 1718813a3359a1c0.jpg ├── 1718813a71f700a1.jpg ├── 1718813abb679031.jpg ├── 1718813afa26977e.jpg ├── 1718813b326b7c73.jpg ├── 1718813b73c47064.jpg ├── 1718813bad18d998.jpg ├── 1718813bec3d6674.jpg ├── 172346f5e5f0ffab.jpg ├── 17237f45a661cbb7.jpg ├── 1723d313056fce6a.jpg ├── 1723e7d3872c9406.jpg ├── 1723fa894b85ed73.jpg ├── 1724037179201fe9.jpg ├── 17240a13fa147bea.jpg ├── 17240afafdc289e5.jpg ├── 1724129062f57007.jpg ├── 172412db1d202759.jpg ├── 17241317342078b9.jpg ├── 17243f19b75d3514.jpg ├── 1727327958931dbc.jpg ├── 172732795e32133c.jpg ├── 17273279684debb7.jpg ├── 1727327968bf0895.jpg ├── 172732797248305f.jpg ├── 172732798545411e.jpg ├── 172732798af59f73.jpg ├── 17273279906f511c.jpg ├── 17273279a6e90503.jpg ├── 17273279bd947317.jpg ├── 1d905286e19f4a3b960d596b184fa8d5.jpeg ├── 2.fc3a2c47.jpg ├── 3.395c1e89.jpg ├── 3.af2b4e20.jpg ├── 4.73e083c6.jpg ├── 4.f3a9aa62.jpg ├── 5.607471fb.jpg ├── 5a043effdcc54f7bb67e3fa13fcbb2cc.jpeg ├── 6289b6c04279481789a6459dc8da2cb1.jpeg ├── 640.jpeg ├── 640.webp ├── 668722-20180910183925685-224282854.png ├── 668722-20180910183940043-66166526.png ├── 668722-20180910184001095-98860783.png ├── 668722-20180910184019522-1324296109.png ├── 668722-20180910184047816-693646977.png ├── 668722-20180910184108826-1909275147.png ├── 668722-20180910184121575-550443104.png ├── 668722-20180910184134043-2011419560.png ├── 668722-20180910184206679-1690711957.png ├── 668722-20180910184223910-1797391394.png ├── 668722-20180910184241818-998562629.png ├── 668722-20180910184255888-841594588.png ├── 745a5d13ff6a4dbc89e287f3188ca111.jpeg ├── 789513781c64485b9130dc239706fbbf.jpeg ├── 94dee06ef9b64c118916593c0a47f40e.jpeg ├── 9823874fa8364612b5a4bdf90b705c4c.jpeg ├── FiaChoSij8ej.webp ├── Shu2ohcohb4j.jpg ├── Wohph0xohfa9.jpg ├── a2e38eaa9f0541769c7569abffe6da48.jpeg ├── a4d3cb957f3f4b35b280118bb129fe58.jpeg ├── af2760bc01cc4da6808cd34087b7176a.jpeg ├── ahwu4aihuo1H.jpg ├── array-intersection-1.png ├── array-intersection-2.png ├── array-intersection-3.png ├── array-intersection-4.png ├── array-intersection.png ├── b68dbf0d64844cf8ab83fca534b04e4a.jpeg ├── b781d9b31ebd4b718de3083cf036fa4b.jpeg ├── bubbleSort.b7d216a5.gif ├── cc285906b45845efa6a5193ec1747ce0.jpeg ├── d33af09c79c04875b884f51b0c752eeb.jpeg ├── dab19075906049dfa6156de45a8bdc1e.jpeg ├── deiGoh6JiePh.webp ├── fd9f917bf17e43d5b7d01ed513e05635.jpeg ├── giethaD1Eice.webp ├── ieHie2ur2ooh.webp ├── iey3Weim7thu.jpg ├── insertionSort.be81c151.gif ├── longest-common-prefix.png ├── lru_comparison.png ├── selectionSort.44be35da.gif └── zaTh5uayeeT1.webp ├── mysql ├── mysql-index-b-plus.md ├── mysql-interview.md └── mysql-mvcc.md ├── question ├── README.md ├── q001.md ├── q002.md ├── q003.md ├── q004.md ├── q005.md ├── q006.md ├── q007.md ├── q008.md ├── q009.md ├── q010.md ├── q011.md ├── q012.md ├── q013.md ├── q014.md ├── q015.md ├── q016.md ├── q017.md ├── q018.md ├── q019.md ├── q020.md ├── q021.md ├── q022.md ├── q023.md └── q024.md ├── redis ├── redis-master-slave.md └── redis-policy.md └── src ├── q001.go ├── q002.go ├── q003.go ├── q004.go ├── q005.go ├── q006.go ├── q007.go ├── q009.go ├── q010.go ├── q011.go ├── q012.go ├── q013.go ├── q014.go ├── q017.go ├── q018.go └── q15_2.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | interview.disign.me -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang 面试题搜集 2 | 3 | ## Golang 常见面试题目解析 4 | 5 | - [交替打印数字和字母](question/q001.md) 6 | - [判断字符串中字符是否全都不同](question/q002.md) 7 | - [翻转字符串](question/q003.md) 8 | - [判断两个给定的字符串排序后是否一致](question/q004.md) 9 | - [字符串替换问题](question/q005.md) 10 | - [机器人坐标计算](question/q006.md) 11 | - [语法题目一](question/q007.md) 12 | - [语法题目二](question/q008.md) 13 | - [goroutine和channel使用一](question/q009.md) 14 | - [实现阻塞读的并发安全Map](question/q010.md) 15 | - [高并发下的锁与map读写问题](question/q011.md) 16 | - [定时与 panic 恢复](question/q012.md) 17 | - [为 sync.WaitGroup 中Wait函数支持 WaitTimeout 功能.](question/q013.md) 18 | - [七道语法找错题目](question/q014.md) 19 | - [golang 并发题目测试](question/q015.md) 20 | - [记一道字节跳动的算法面试题](question/q016.md) 21 | - [多协程查询切片问题](question/q017.md) 22 | - [对已经关闭的的chan进行读写,会怎么样?为什么?](question/q018.md) 23 | - [简单聊聊内存逃逸?](question/q019.md) 24 | - [字符串转成byte数组,会发生内存拷贝吗?](question/q020.md) 25 | - [http包的内存泄漏](question/q021.md) 26 | - [sync.Map 的用法](question/q022.md) 27 | - [Golang基础语法相关题目](question/q023.md) 28 | - [go 面试题:连接字符串有几种方法](question/q024.md) 29 | 30 | ## Golang 理论 31 | 32 | - [Go语言的GPM调度器是什么?](base/go-gpm.md) 33 | - [Goroutine调度策略](base/go-scheduler.md) 34 | - [goroutine调度器概述](base/go-scheduler-base.md) 35 | - [Golang的垃圾回收机制是如何工作的?](https://study.disign.me/article/202502/35.Golang垃圾回收(GC)介绍.md) 36 | - [golang中如果出现了内存泄露, 怎么排查?一般都有哪些情况可能导致内存泄露](https://study.disign.me/article/202502/36.Go%E7%A8%8B%E5%BA%8F%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D.md) 37 | - [讲讲GMP模型](https://study.disign.me/article/202502/38.go-GPM%E6%A8%A1%E5%9E%8B.md) 38 | - [Golang常见语法面试题总结](base/go-grammar.md) 39 | 40 | 41 | ## Redis基础 42 | 43 | - [Redis 基础数据结构](base/redis.md) 44 | - [Redis中的底层数据结构](base/redis-data-structure.md) 45 | - [Redis持久化的原理及优化](base/redis-rdb.md) 46 | - [Redis中内存淘汰算法实现](redis/redis-policy.md) 47 | - [Redis主从复制原理](redis/redis-master-slave.md) 48 | 49 | ## MySQL相关 50 | 51 | - [MySQL数据库经典面试题解析](mysql/mysql-interview.md) 52 | - [MySQL InnoDB MVCC 机制的原理及实现](mysql/mysql-mvcc.md) 53 | - [为什么MySQL使用B+树做索引?](mysql/mysql-index-b-plus.md) 54 | 55 | ## 面试必备算法 56 | 57 | - [字符串之实现 Sunday 匹配](algorithm/docs/match-sunday-string.md) 58 | - [字符串泄漏之反转字符串(301)](algorithm/docs/reverse-string.md) 59 | - [字符串中的第一个唯一字符](algorithm/docs/search-first-unique-char.md) 60 | - [字符串之验证回文串](algorithm/docs/verify-palindrome.md) 61 | - [滑动窗口最大值](algorithm/docs/sliding-window-maximum.md) 62 | - [最长公共前缀](algorithm/docs/longest-common-prefix.md) 63 | - [两个数组的交集](algorithm/docs/array-intersection.md) 64 | - [最接近的三数之和](algorithm/docs/3sum-closest.md) 65 | 66 | ### 排序算法 67 | 68 | - [冒泡排序](algorithm/docs/bubble-sort.md) 69 | - [选择排序](algorithm/docs/selection-sort.md) -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /algorithm/3sum-closest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | ) 8 | 9 | func main() { 10 | nums := []int{-1, 2, 1, -2, -4} 11 | target := 1 12 | fmt.Println(threeSumClosest(nums, target)) 13 | } 14 | 15 | func threeSumClosest(nums []int, target int) int { 16 | if len(nums) == 3 { 17 | return nums[0] + nums[1] + nums[2] 18 | } 19 | sort.Ints(nums) 20 | sum := nums[0] + nums[1] + nums[2] 21 | 22 | for i := 0; i < len(nums); i++ { 23 | l := i + 1 24 | r := len(nums) - 1 25 | for l < r { 26 | current := nums[i] + nums[l] + nums[r] 27 | if math.Abs(float64(sum-current)) > math.Abs(float64(target-current)) { 28 | sum = current 29 | } 30 | if current < target { 31 | l++ 32 | } else if current == target { 33 | return target 34 | } else { 35 | r-- 36 | } 37 | } 38 | } 39 | return sum 40 | } 41 | -------------------------------------------------------------------------------- /algorithm/add-binary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | 5 | } 6 | 7 | /** 8 | * 9 | * @param a string字符串 10 | * @param b string字符串 11 | * @return string字符串 12 | */ 13 | func addBinary(a string, b string) string { 14 | // write code here 15 | return "" 16 | } 17 | -------------------------------------------------------------------------------- /algorithm/array-intersection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //两个数组的交集 6 | //给定两个数组,编写一个函数来计算它们的交集。 7 | //示例 1: 8 | // 9 | //输入: nums1 = [1,2,2,1], nums2 = [2,2] 10 | // 11 | //输出: [2,2] 12 | //示例 2: 13 | // 14 | //输入: nums1 = [4,9,5,9], nums2 = [9,4,9,8,4] 15 | // 16 | //输出: [4,9,9] 17 | //说明: 18 | // 19 | //输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。 20 | //我们可以不考虑输出结果的顺序。 21 | //进阶: 22 | // 23 | //如果给定的数组已经排好序呢?将如何优化你的算法呢? 24 | //思路:设定两个为0的指针,比较两个指针的元素是否相等。如果指针的元素相等,我们将两个指针一起向后移动,并且将相等的元素放入空白数组。 25 | func main() { 26 | nums1 := []int{4, 9, 5, 9} 27 | nums2 := []int{9, 4, 8, 4, 9, 5, 5} 28 | 29 | fmt.Println("无序数组 ->", intersect(nums1, nums2)) 30 | 31 | nums1 = []int{1, 2, 3, 4, 5, 13} 32 | nums2 = []int{1, 2, 5, 9, 10} 33 | 34 | fmt.Println("有序数组 ->", intersectSort(nums1, nums2)) 35 | } 36 | 37 | //无序数组 38 | func intersect(nums1 []int, nums2 []int) []int { 39 | m0 := make(map[int]int) 40 | for _, i := range nums1 { 41 | m0[i] += 1 42 | } 43 | k := 0 44 | for _, v := range nums2 { 45 | if m0[v] > 0 { 46 | m0[v] -= 1 47 | //这里是复用切片 48 | nums2[k] = v 49 | k++ 50 | } 51 | } 52 | return nums2[0:k] 53 | } 54 | 55 | //有序数组 56 | func intersectSort(nums1 []int, nums2 []int) []int { 57 | 58 | i, j, m := 0, 0, 0 59 | for i < len(nums1) && j < len(nums2) { 60 | if nums1[i] == nums2[j] { 61 | nums1[m] = nums1[i] 62 | m++ 63 | i++ 64 | j++ 65 | } else if nums1[i] > nums2[j] { 66 | j++ 67 | } else { 68 | i++ 69 | } 70 | } 71 | return nums1[:m] 72 | } 73 | -------------------------------------------------------------------------------- /algorithm/docs/3sum-closest.md: -------------------------------------------------------------------------------- 1 | # 最接近的三数之和 2 | 3 | ## 三数之和 4 | 5 | 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。 6 | 7 | **示例:** 8 | 9 | >给定数组 nums = [-1, 0, 1, 2, -1, -4], 10 | > 11 | >满足要求的三元组集合为: 12 | > 13 | >[ 14 | [-1, 0, 1], 15 | [-1, -1, 2] 16 | ] 17 | 18 | ## 02、题目分析 19 | 20 | > 本题的暴力题解可以仿照二数之和,直接三层遍历,取和为0的三元组,并记录下来,最后再去重。但是作为一个有智慧的人,我们不能这么去做。 21 | 22 | 23 | 因为我们的目标是找数,当然使用指针的方式最简单。假若我们的数组为: 24 | 25 | > [-1, 0, 1, 2, -1, -4] 26 | 27 | 求解过程如下:首先我们先把数组排个序(原因一会儿说),排完序长这样: 28 | 29 | ![](../../images/1.0507c071.jpg) 30 | 31 | 因为我们要同时找三个数,所以采取固定一个数,同时用双指针来查找另外两个数的方式。所以初始化时,我们选择固定第一个元素(当然,这一轮走完了,这个蓝框框我们就要也往前移动),同时将下一个元素和末尾元素分别设上 left 和 right 指针。画出图来就是下面这个样子: 32 | 33 | ![](../../images/2.5e72397e.jpg) 34 | 35 | 现在已经找到了三个数,当然是计算其三值是否满足三元组。但是这里因为我们已经排好了序,如果固定下来的数(上面蓝色框框)本身就大于 0,那三数之和必然无法等于 0。比如下面这种: 36 | 37 | ![](../../images/3.bb5bf3f0.jpg) 38 | 39 | 然后自然用脚指头也能想到,我们需要移动指针。现在我们的排序就发挥出用处了,如果和大于0,那就说明 right 的值太大,需要左移。如果和小于0,那就说明 left 的值太小,需要右移。(上面这个思考过程是本题的核心) 整个过程如下图所示: 40 | 41 | ![](../../images/4.78f043f5.jpg) 42 | 43 | 其中:在第6行时,因为三数之和大于0,所以right进行了左移。最后一行,跳过了重复的-1。 44 | 45 | 46 | 然后啰嗦一句,因为我们需要处理重复值的情况。除了固定下来的i值(蓝框框),left 和 right 当然也是需要处理重复的情况,所以对于 left 和 left+1,以及 right 和 right-1,我们都单独做一下重复值的处理。(其实没啥处理,就是简单的跳过) 47 | 48 | ## 03、代码展示 49 | 50 | >四数之和其实与本题解法差不太多,把固定一个数变成两个,同样还是使用双指针进行求解就可以了。 51 | 52 | Golang版本实现: 53 | 54 | ```go 55 | func threeSumClosest(nums []int, target int) int { 56 | if len(nums) == 3 { 57 | return nums[0] + nums[1] + nums[2] 58 | } 59 | sort.Ints(nums) 60 | sum := nums[0] + nums[1] + nums[2] 61 | 62 | for i := 0; i < len(nums); i++ { 63 | l := i+1 64 | r := len(nums) - 1 65 | for l < r { 66 | current := nums[i] + nums[l] + nums[r] 67 | if math.Abs(float64(sum - target)) > math.Abs(float64(target - current)) { 68 | sum = current 69 | } 70 | if current < target { 71 | l++ 72 | } else if current == target { 73 | return target 74 | } else { 75 | r-- 76 | } 77 | } 78 | } 79 | return sum 80 | } 81 | ``` 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /algorithm/docs/array-intersection.md: -------------------------------------------------------------------------------- 1 | # 两个数组的交集 2 | 3 | ## 01、题目分析 4 | 5 | 我们先来看一道题目: 6 | 7 | ### 第350题:两个数组的交集 8 | 9 | 给定两个数组,编写一个函数来计算它们的交集。 10 | 11 | **示例 1:** 12 | 13 | >输入: nums1 = [1,2,2,1], nums2 = [2,2] 14 | 15 | >输出: [2,2] 16 | 17 | **示例 2:** 18 | 19 | >输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4] 20 | 21 | >输出: [4,9] 22 | 23 | **说明:** 24 | 25 | - 输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。 26 | - 我们可以不考虑输出结果的顺序。 27 | 28 | 进阶: 29 | 30 | - 如果给定的数组已经排好序呢?将如何优化你的算法呢? 31 | 32 | 思路:设定两个为0的指针,比较两个指针的元素是否相等。如果指针的元素相等,我们将两个指针一起向后移动,并且将相等的元素放入空白数组。 33 | 34 | ## 02、题解分析 35 | 36 | 首先拿到这道题,我们基本马上可以想到,此题可以看成是一道传统的映射题(map映射),为什么可以这样看呢,因为我们需找出两个数组的交集元素,同时应与两个数组中出现的次数一致。这样就导致了我们需要知道每个值出现的次数,所以映射关系就成了<元素,出现次数>。剩下的就是顺利成章的解题。 37 | 38 | 由于该种解法过于简单,我们不做进一步分析,直接给出题解: 39 | 40 | ```go 41 | func intersect(nums1 []int, nums2 []int) []int { 42 | m0 := make(map[int]int) 43 | for _, i := range nums1 { 44 | m0[i] += 1 45 | } 46 | k := 0 47 | for _, v := range nums2 { 48 | if m0[v] > 0 { 49 | m0[v] -= 1 50 | //这里是复用切片 51 | nums2[k] = v 52 | k++ 53 | } 54 | } 55 | return nums2[0:k] 56 | } 57 | ``` 58 | 59 | 这个方法比较简单,相信大家都能看的懂! 60 | 61 | ## 03、题目进阶 62 | 63 | 题目在进阶问题中问道:如果给定的数组已经排好序呢?你将如何优化你的算法?我们分析一下,假如两个数组都是有序的,分别为:`arr1 = [1,2,3,4,4,13]`,`arr2 = [1,2,3,9,10]` 64 | 65 | ![](../../images/array-intersection.png) 66 | 67 | 对于两个已经排序好数组的题,我们可以很容易想到使用双指针的解法~ 68 | 69 | **解题步骤如下:** 70 | 71 | <1>设定两个为0的指针,比较两个指针的元素是否相等。 如果指针的元素相等,我们将两个指针一起向后移动,并且将相等的元素放入空白数组。下图中我们的指针分别指向第一个元素,判断元素相等之后,将相同元素放到空白的数组。 72 | 73 | ![](../../images/array-intersection-1.png) 74 | 75 | <2>如果两个指针的元素不相等,我们将小的一个指针后移。 图中我们指针移到下一个元素,判断不相等之后,将元素小的指针向后移动,继续进行判断。 76 | 77 | ![](../../images/array-intersection-2.png) 78 | 79 | <3>反复以上步骤。 80 | 81 | ![](../../images/array-intersection-3.png) 82 | 83 | <4>直到任意一个数组终止。 84 | 85 | ![](../../images/array-intersection-4.png) 86 | 87 | ## 04、题目解答 88 | 89 | 根据分析,我们很容易得到下面的题解: 90 | 91 | ```go 92 | func intersect(nums1 []int, nums2 []int) []int { 93 | 94 | i, j,m := 0, 0,0 95 | for i < len(nums1) && j < len(nums2) { 96 | if nums1[i] == nums2[j] { 97 | nums1[m] = nums1[i] 98 | m++ 99 | i++ 100 | j++ 101 | } else if nums1[i] > nums2[j] { 102 | j++ 103 | } else { 104 | i++ 105 | } 106 | } 107 | return nums1[:m] 108 | } 109 | ``` 110 | 111 | **提示:** 解答中我们并没有创建空白数组,因为遍历后的数组其实就没用了。我们可以将相等的元素放入用过的数组中,就为我们节省下了空间。 -------------------------------------------------------------------------------- /algorithm/docs/bubble-sort.md: -------------------------------------------------------------------------------- 1 | # 冒泡排序 2 | 3 | 冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 4 | 5 | 作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。 6 | 7 | ## 1. 算法步骤 8 | 9 | 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 10 | 11 | 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 12 | 13 | 针对所有的元素重复以上的步骤,除了最后一个。 14 | 15 | 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 16 | 17 | ## 2. 动图演示 18 | 19 | ![](../../images/bubbleSort.b7d216a5.gif) 20 | 21 | ## 3. 最慢和最快 22 | 23 | 正序时最快,反序时最慢 24 | ## Golang实现 25 | 26 | ```go 27 | func bubbleSort(arr []int) []int { 28 | if len(arr) == 0 { 29 | return arr 30 | } 31 | for i := 0; i < len(arr); i++ { 32 | for j := 0; j < len(arr); j++ { 33 | if arr[i] > arr[j] { 34 | arr[j], arr[i] = arr[i], arr[j] 35 | } 36 | } 37 | } 38 | return arr 39 | } 40 | ``` 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /algorithm/docs/insertion-sort.md: -------------------------------------------------------------------------------- 1 | # 插入排序 2 | 3 | 插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 4 | 5 | 插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。 6 | 7 | ## 1. 算法步骤 8 | 9 | - 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 10 | - 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。) 11 | 12 | ## 2. 动图演示 13 | 14 | ![](../../images/insertionSort.be81c151.gif) 15 | 16 | ## 3. Golang 实现 17 | 18 | ```go 19 | func insertionSort(arr []int) []int { 20 | l := len(arr) 21 | if l == 0 { 22 | return arr 23 | } 24 | for i := 0; i < l - 1; i++ { 25 | for j := i + 1; j > 0; j-- { 26 | if arr[j] < arr[j - 1] { 27 | arr[j],arr[j - 1] = arr[j - 1],arr[j] 28 | } 29 | } 30 | } 31 | return arr 32 | } 33 | ``` -------------------------------------------------------------------------------- /algorithm/docs/longest-common-prefix.md: -------------------------------------------------------------------------------- 1 | # 最长公共前缀 2 | 3 | ## 01、题目分析 4 | 5 | 首先还是看下题目: 6 | 7 | ### 题目14: 最长公共前缀 8 | 9 | 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,则返回"" 10 | 11 | 示例1: 12 | 13 | >输入: ["flower","flow","flight"] 14 | >输出: "fl" 15 | 16 | 示例 2: 17 | 18 | >输入: ["dog","racecar","car"] 19 | >输出: "" 20 | >解释: 输入不存在公共前缀。 21 | 22 | **解释:** 23 | 24 | - 输入不存在公共前缀。 25 | 26 | **说明:** 27 | 28 | - 所有输入只包含小写字母 a-z 29 | 30 | ## 02、题解分析 31 | 32 | 我们要想寻找最长公共前缀,那么首先这个前缀是公共的,我们可以从任意一个元素中找到它。假定我们现在就从一个数组中寻找最长公共前缀,那么首先,我们可以将第一个元素设置为基准元素x0。假如数组为["flow","flower","flight"],flow就是我们的基准元素x0。 33 | 34 | 然后我们只需要依次将基准元素和后面的元素进行比较(假定后面的元素依次为x1,x2,x3....),不断更新基准元素,直到基准元素和所有元素都满足最长公共前缀的条件,就可以得到最长公共前缀。 35 | 36 | 具体比对过程如下: 37 | 38 | - 如果strings.Index(x1,x) == 0,则直接跳过(因为此时x就是x1的最长公共前缀),对比下一个元素。(如flower和flow进行比较) 39 | - 如果strings.Index(x1,x) != 0, 则截取掉基准元素x的最后一个元素,再次和x1进行比较,直至满足string.Index(x1,x) == 0,此时截取后的x为x和x1的最长公共前缀。(如flight和flow进行比较,依次截取出flow-flo-fl,直到fl被截取出,此时fl为flight和flow的最长公共前缀) 40 | 41 | 具体过程如下图所示: 42 | 43 | ![](../../images/longest-common-prefix.png) 44 | 45 | 46 | >我们需要注意的是,在处理基准元素的过程中,如果基准元素和任一一个元素无法匹配,则说明不存在最长公共元素。 47 | 48 | 最后,我们记得处理一下临界条件。如果给定数组是空,也说明没有最长公共元素。 49 | 50 | 然后我们就可以开始写我们的代码了。 51 | 52 | ## 03、代码分析 53 | 54 | 题解可以很自然的给出,我们先给一个 go 的版本: 55 | 56 | ```go 57 | func getPrefix(arr []string) string { 58 | if len(arr) <= 1 { 59 | return "" 60 | } 61 | firstStr := arr[0] 62 | l := len(arr) 63 | for i := range firstStr { 64 | for j := 1; j < l; j++ { 65 | if arr[j][i] != firstStr[i] { 66 | return firstStr[:i] 67 | } 68 | } 69 | } 70 | return "" 71 | } 72 | ``` 73 | 74 | 使用内置函数版: 75 | 76 | ```go 77 | //GO 78 | func longestCommonPrefix(strs []string) string { 79 | if len(strs) < 1 { 80 | return "" 81 | } 82 | prefix := strs[0] 83 | for _,k := range strs { 84 | for strings.Index(k,prefix) != 0 { 85 | if len(prefix) == 0 { 86 | return "" 87 | } 88 | prefix = prefix[:len(prefix) - 1] 89 | } 90 | } 91 | return prefix 92 | } 93 | ``` 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /algorithm/docs/match-sunday-string.md: -------------------------------------------------------------------------------- 1 | # 实现 Sunday 匹配 2 | 3 | ## 01、实现 strStr() 4 | 5 | ### 题目:实现 strStr() 6 | 7 | 实现 strStr() 函数。给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。 8 | 9 | 示例 1: 10 | 11 | >输入: haystack = "hello", needle = "ll" 12 | >输出: 2 13 | 14 | 示例 2: 15 | 16 | >输入: haystack = "aaaaa", needle = "bba" 17 | >输出: -1 18 | 19 | **说明:** 20 | 21 | 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 22 | 23 | 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 24 | 25 | # 02、Sunday 匹配 26 | 27 | Sunday 算法是 Daniel M.Sunday 于1990年提出的字符串模式匹配。其核心思想是:在匹配过程中,模式串发现不匹配时,算法能跳过尽可能多的字符以进行下一步的匹配,从而提高了匹配效率。 28 | 29 | 30 | 因为该问是字符串匹配篇第一讲,所以先普及几个概念: 31 | 32 | - 串:串是字符串的简称 33 | - 空串:长度为零的串称为空串 34 | - 主串:包含子串的串相应地称为主串 35 | - 子串:串中任意个连续字符组成的子序列称为该串的子串 36 | - 模式串:子串的定位运算又称为串的模式匹配,是一种求子串第一个字符在主串中序号的运算。被匹配的主串称为目标串,子串称为模式串。 37 | 38 | 了解这些基本概念,回到这个算法。Sunday匹配不是说这人在周末发现了这个算法,而是这人名字叫星期天(可能父母总加班,所以起了这么个名)。听起来牛叉的不得了,其实是个啥意思: 39 | 40 | **假若我们的目标串为:Here is a little Hao** 41 | 42 | **模式串为:little** 43 | 44 | 一般来讲,字符串匹配算法第一步,都是把目标串和模式串对齐。不管是KMP,BM,SUNDAY都是这样。 45 | 46 | ![](../../images/1.f441ca2d.jpg) 47 | 48 | 而对于SUNDAY算法,我们从头部开始比较,一旦发现不匹配,直接找到主串中位于模式串后面的第一个字符,即下面绿色的 “s”。(这里说明一下,为什么是找模式串后面的第一个字符。在把模式串和目标串对齐后,如果发现不匹配,那肯定需要移动模式串。问题是需要移动多少步。各字符串匹配算法之间的差别也来自于这个地方,对于KMP,是建立部分匹配表来计算。BM,是反向比较计算移动量。对于SUNDAY,就是找到模式串后的第一个字符。因为,无论模式串移动多少步,模式串后的第一个字符都要参与下一次比较,也就是这里的 “s”) 49 | 50 | ![](../../images/2.fc3a2c47.jpg) 51 | 52 | 找到了模式串后的第一个字符 “s”,接下来该怎么做?我们需要查看模式串中是否包含这个元素,如果不包含那就可以跳过一大片,从该字符的下一个字符开始比较。 53 | 54 | ![](../../images/3.af2b4e20.jpg) 55 | 56 | 因为仍然不匹配(空格和l),我们继续重复上面的过程。找到模式串的下一个元素:t 57 | 58 | ![](../../images/4.73e083c6.jpg) 59 | 60 | 现在有意思了,我们发现 t 被包含于模式串中,并且 t 出现在模式串倒数第3个。所以我们把模式串向前移动3个单位: 61 | 62 | ![](../../images/5.607471fb.jpg) 63 | 64 | 有内味了,我们发现竟然匹配成功了,是不是很神奇?证明的过程今天暂且不谈(后面我会出一个算法证明篇,来证明之前讲过的一些算法。我需要你做的是,掌握上面这些!) 65 | 66 | 捞干货,这个过程里我们做了一些什么: 67 | 68 | - 对齐目标串和模式串,从前向后匹配 69 | - 关注主串中位于模式串后面的第一个元素(核心) 70 | - 如果关注的字符没有在子串中出现则直接跳过 71 | - 否则开始移动模式串,移动位数 = 子串长度 - 该字符最右出现的位置(以0开始) 72 | 73 | ## 03、算法应用 74 | 75 | 自然是把这个算法应用到我们的题目中咯... 76 | 77 | 78 | 根据分析,得出代码:(给一个保证你能看的懂的Golang版本的) 79 | 80 | ```go 81 | func strStrSunday(haystack, needle string) int { 82 | //先判断两个字符串的合法性 83 | if len(haystack) < len(needle) { 84 | return -1 85 | } 86 | if haystack == needle { 87 | return 0 88 | } 89 | //定义最终位置的索引 90 | index := -1 91 | i := 0 92 | //定义目标匹配索引 93 | needleIndex := 0 94 | for i < len(haystack) { 95 | //逐字节判断是否相等 96 | if haystack[i] == needle[needleIndex] { 97 | //只有当index为-1时,说明是首次匹配到字符 98 | if index == -1 { 99 | index = i 100 | } 101 | //主串索引和模式串索引都自增 102 | i++ 103 | needleIndex++ 104 | //判断是否完成匹配 105 | if needleIndex >= len(needle) { 106 | break 107 | } 108 | continue 109 | } 110 | //走到这里说明没有匹配成功,将匹配目标索引置为默认 111 | index = -1 112 | //计算主串需要移动的位置 113 | i = i + len(needle) - needleIndex 114 | //如果主串索引大于了主串实际长度则返回 115 | if i >= len(haystack) { 116 | return index 117 | } 118 | //计算下一个字符在模式串最右的位置 119 | offset := 1 120 | for j := len(needle) - 1; j > 0; j-- { 121 | if haystack[i] == needle[j] { 122 | offset = j 123 | break 124 | } 125 | } 126 | //将主串的索引左移指定长度,使当前的字符和模式串中最右匹配到的字符串对齐 127 | i = i - offset 128 | //将模式串的索引重置 129 | needleIndex = 0 130 | } 131 | return index 132 | } 133 | 134 | ``` 135 | 136 | ## 来源 137 | 138 | > [实现 Sunday 匹配](https://www.geekxh.com/1.3.%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%B3%BB%E5%88%97/303.html#_03%E3%80%81%E7%AE%97%E6%B3%95%E5%BA%94%E7%94%A8) 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /algorithm/docs/reverse-string.md: -------------------------------------------------------------------------------- 1 | # 反转字符串(301) 2 | 3 | ## 01、题目分析 4 | 5 | ### 第344题:反转字符串 6 | 7 | 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 8 | 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 9 | 10 | **示例 1:** 11 | 12 | >输入:["h","e","l","l","o"] 13 | >输出:["o","l","l","e","h"] 14 | 15 | 示例 2: 16 | 17 | >输入:["H","a","n","n","a","h"] 18 | >输出:["h","a","n","n","a","H"] 19 | 20 | ## 02、题目图解 21 | 22 | 这是一道相当简单的经典题目,直接上题解:使用双指针进行反转字符串。 23 | 24 | 假设输入字符串为`["h","e","l","l","0"]` 25 | 26 | - 定义left和right分别指向首元素和尾元素 27 | - 当`left < right` ,进行交换。 28 | - 交换完毕,`left++,right--` 29 | - 直至`left == right` 30 | 31 | 具体过程如下图所示: 32 | 33 | ![](../../images/1.81971505.jpg) 34 | 35 | ## 03、Go语言示例 36 | 37 | 根据以上分析,我们可以得到下面的题解: 38 | 39 | ```go 40 | func Reverse(s []byte) { 41 | right := len(s) - 1 42 | left := 0 43 | 44 | for left < right { 45 | s[left], s[right] = s[right], s[left] 46 | left++ 47 | right-- 48 | } 49 | } 50 | ``` 51 | 52 | ## 原文 53 | 54 | > [反转字符串(301)](https://www.geekxh.com/1.3.%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%B3%BB%E5%88%97/301.html#_02%E3%80%81%E9%A2%98%E7%9B%AE%E5%9B%BE%E8%A7%A3) -------------------------------------------------------------------------------- /algorithm/docs/search-first-unique-char.md: -------------------------------------------------------------------------------- 1 | # 字符串中的第一个唯一字符 2 | 3 | ## 01、题目分析 4 | 5 | ### 第387题:字符串中的第一个唯一字符 6 | 7 | 给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1 。 8 | 案例: 9 | 10 | >s = "leetcode" 11 | >返回 0. 12 | 13 | >s = "loveleetcode", 14 | >返回 2. 15 | 16 | **注意事项: 您可以假定该字符串只包含小写字母。** 17 | 18 | 常考题目,建议自行思考 1-2 分钟先~ 19 | 20 | ## 02、题目图解 21 | 22 | 题目不难,直接进行分析。由于字母共有 26 个,所以我们可以声明一个 26 个长度的数组(该种方法在本类题型很常用)因为字符串中字母可能是重复的,所以我们可以先进行第一次遍历,在数组中记录每个字母的最后一次出现的所在索引。然后再通过一次循环,比较各个字母第一次出现的索引是否为最后一次的索引。如果是,我们就找到了我们的目标,如果不是我们将其设为 -1(标示该元素非目标元素)如果第二次遍历最终没有找到目标,直接返回 -1即可。 23 | 24 | 25 | 图解如下: 26 | 27 | ![](../../images/1.458698c8.jpg) 28 | 29 | ## 03、GO语言示例 30 | 31 | 根据以上分析,可以得到代码如下: 32 | 33 | ```go 34 | func firstUniqueChar(s string) int { 35 | var arr [26]int 36 | //第一次遍历计算所有字符出现的最后位置 37 | for i, k := range s { 38 | //因为26个字母从a开始,减去a则索引会从0开始 39 | arr[k-'a'] = i 40 | } 41 | for i, k := range s { 42 | if arr[k-'a'] == i { 43 | return i 44 | } 45 | } 46 | return -1 47 | } 48 | ``` -------------------------------------------------------------------------------- /algorithm/docs/selection-sort.md: -------------------------------------------------------------------------------- 1 | # 选择排序 2 | 3 | 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。 4 | 5 | ## 1. 算法步骤 6 | 7 | - 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 8 | - 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 9 | - 重复第二步,直到所有元素均排序完毕。 10 | 11 | ## 2. 动图演示 12 | 13 | ![](../../images/selectionSort.44be35da.gif) 14 | 15 | ## 3. Go 代码实现 16 | 17 | ```go 18 | func selectionSort(arr []int) []int { 19 | l := len(arr) 20 | if l == 0 { 21 | return arr 22 | } 23 | for i := 0; i < l; i++ { 24 | min := i 25 | for j := i + 1; j < l; j++ { 26 | if arr[j] < arr[min] { 27 | min = j 28 | } 29 | } 30 | arr[i],arr[min] = arr[min],arr[i] 31 | } 32 | return arr 33 | } 34 | ``` 35 | 36 | 37 | -------------------------------------------------------------------------------- /algorithm/docs/sliding-window-maximum.md: -------------------------------------------------------------------------------- 1 | # 滑动窗口最大值 2 | 3 | ## 01、题目分析 4 | 5 | ### 第239题:滑动窗口最大值 6 | 7 | 给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。 8 | 9 | 10 | 返回滑动窗口中的最大值所构成的数组。 11 | 12 | 13 | 示例: 14 | 15 | >输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 16 | >输出: [3,3,5,5,6,7] 17 | >解释: 18 | > 19 | > 滑动窗口的位置 最大值 20 | >--------------- ----- 21 | >[1 3 -1] -3 5 3 6 7 3 22 | > 23 | > 1 [3 -1 -3] 5 3 6 7 3 24 | > 25 | > 1 3 [-1 -3 5] 3 6 7 5 26 | > 27 | > 1 3 -1 [-3 5 3] 6 7 5 28 | > 29 | > 1 3 -1 -3 [5 3 6] 7 6 30 | > 31 | > 1 3 -1 -3 5 [3 6 7] 7 32 | 33 | ## 02、题目分析 34 | 35 | 本题对于题目没有太多需要额外说明的,应该都能理解,直接进行分析。我们很容易想到,可以通过遍历所有的滑动窗口,找到每一个窗口的最大值,来进行暴力求解。那一共有多少个滑动窗口呢,小学题目,可以得到共有 L-k+1 个窗口。 36 | 37 | 38 | 假设 `nums = [1,3,-1,-3,5,3,6,7]`,和 `k = 3`,窗口数为`6`: 39 | 40 | ![](../../images/1.fc3a2c47.jpg) 41 | 42 | 根据分析,直接完成代码: 43 | 44 | ```go 45 | func maxSlidingWindow(nums []int, k int) []int { 46 | l1 := len(nums) 47 | index := 0 48 | ret := make([]int, 0) 49 | for index < l1 { 50 | m := nums[index] 51 | if index > l1 - k { 52 | break 53 | } 54 | for j := index + 1; j < index + k; j++ { 55 | if m < nums[j] { 56 | m = nums[j] 57 | } 58 | } 59 | ret = append(ret,m) 60 | index++ 61 | } 62 | return ret 63 | } 64 | ``` 65 | 66 | ## 03、线性题解 67 | 68 | 这里不卖关子,其实这道题比较经典,我们可以采用队列,DP,堆等方式进行求解,所有思路的主要源头应该都是在窗口滑动的过程中,如何更快的完成查找最大值的过程。但是最典型的解法还是使用双端队列。具体怎么来求解,一起看一下。 69 | 70 | 71 | 首先,我们了解一下,什么是双端队列:是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出或者插入。 72 | 73 | ![](../../images/3.395c1e89.jpg) 74 | 75 | 我们可以利用双端队列来实现一个窗口,目的是让该窗口可以做到张弛有度(汉语博大精深,也就是长度动态变化。其实用游标或者其他解法的目的都是一样的,就是去维护一个可变长的窗口) 76 | 77 | 然后我们再做一件事,只要遍历该数组,同时**在双端队列的头去维护当前窗口的最大值(在遍历过程中,发现当前元素比队列中的元素大,就将原来队列中的元素祭天),在整个遍历的过程中我们再记录下每一个窗口的最大值到结果数组中。**最终结果数组就是我们想要的,整体图解如下。 78 | 79 | 假设 `nums = [1,3,-1,-3,5,3,6,7]`,和 `k = 3`: 80 | 81 | ![](../../images/4.f3a9aa62.jpg) 82 | 83 | 根据分析,得出代码: 84 | 85 | ```go 86 | func maxSlidingWindow2(nums []int, k int) []int { 87 | ret := make([]int,0) 88 | if len(nums) == 0 { 89 | return ret 90 | } 91 | var queue []int 92 | for i := range nums { 93 | for i > 0 && (len(queue) > 0) && nums[i] > queue[len(queue)-1] { 94 | //将比当前元素小的元素祭天 95 | queue = queue[:len(queue)-1] 96 | } 97 | //将当前元素放入queue中 98 | queue = append(queue, nums[i]) 99 | if i >= k && nums[i-k] == queue[0] { 100 | //维护队列,保证其头元素为当前窗口最大值 101 | queue = queue[1:] 102 | } 103 | if i >= k-1 { 104 | //放入结果数组 105 | ret = append(ret, queue[0]) 106 | } 107 | } 108 | return ret 109 | } 110 | ``` -------------------------------------------------------------------------------- /algorithm/docs/verify-palindrome.md: -------------------------------------------------------------------------------- 1 | # 验证回文串 2 | 3 | ## 01、题目示例 4 | 5 | > 见微知著,发现一组数据很有趣,分享给大家。leetcode 第一题通过次数为 993,335,第二题通过次数为 396,160,第三题通过次数为 69,508。我想说什么,请自己悟。 6 | 7 | ### 第125题:验证回文串 8 | 9 | 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 10 | 11 | 说明: 本题中,我们将空字符串定义为有效的回文串。 12 | 13 | > 示例 1: 14 | >输入: "A man, a plan, a canal: Panama" 15 | >输出: true 16 | 17 | >示例 2: 18 | >输入: "race a car" 19 | >输出: false 20 | 21 | ## 02、图解教程 22 | 23 | 经典题目,你需要像掌握反转字符串一样掌握本题。 24 | 25 | 首先,我想确保你知道什么是回文串。“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。 26 | 27 | 对于字符串中可能存在的其他字符,可以通过正则替换,但是正则替换会增加程序运行复杂度,下面给出的是在判断过程中忽略其他字符: 28 | 29 | ```go 30 | func isPalindrome(s string) bool { 31 | if s == "" { 32 | return false 33 | } 34 | s = strings.ToLower(s) 35 | if len(s) == 2 { 36 | return s[0] == s[1] 37 | } 38 | left := 0 39 | right := len(s) - 1 40 | for left < right { 41 | //忽略除字母和数字之外的字符 42 | if !((s[left] >= 'a' && s[left] <= 'z') || (s[left] >= '0' && s[left] <= '9')) { 43 | left++ 44 | continue 45 | } 46 | if !((s[right] >= 'a' && s[right] <= 'z') || (s[right] >= '0' && s[right] <= '9')){ 47 | right-- 48 | continue 49 | } 50 | if s[left] != s[right] { 51 | return false 52 | } 53 | left++ 54 | right-- 55 | } 56 | return true 57 | } 58 | ``` -------------------------------------------------------------------------------- /algorithm/first-unique-char.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //字符串中的第一个唯一字符 6 | //给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1 。 7 | //案例: 8 | // 9 | //s = "leetcode" 10 | //返回 0. 11 | // 12 | //s = "loveleetcode", 13 | //返回 2. 14 | //注意事项: 您可以假定该字符串只包含小写字母。 15 | func main() { 16 | s := "loveleetcode" 17 | i := firstUniqueChar(s) 18 | fmt.Println(i) 19 | } 20 | 21 | func firstUniqueChar(s string) int { 22 | var arr [26]int 23 | //第一次遍历计算所有字符出现的最后位置 24 | for i, k := range s { 25 | arr[k-'a'] = i 26 | } 27 | for i, k := range s { 28 | if arr[k-'a'] == i { 29 | return i 30 | } 31 | } 32 | return -1 33 | } 34 | -------------------------------------------------------------------------------- /algorithm/ip-start-end-query.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type IPAddr struct { 14 | start string 15 | end string 16 | addr string 17 | } 18 | 19 | var ( 20 | ipMap = make(map[int]IPAddr) 21 | ipIndex = make([]int, 0) 22 | ) 23 | 24 | //查找指定ip段与地址映射文件中的ip地址 25 | func main() { 26 | ip := "172.168.10.210" 27 | addr := query(ip) 28 | fmt.Println(addr) 29 | 30 | } 31 | func query(ip string) string { 32 | ipInt := ipToInt(ip) 33 | fmt.Println(ipInt) 34 | for i := len(ipIndex) - 1; i >= 0; i-- { 35 | if ipInt >= ipIndex[i] { 36 | addr, _ := ipMap[ipIndex[i]] 37 | return addr.addr 38 | } 39 | } 40 | return "" 41 | } 42 | 43 | func initIPAddress() error { 44 | f, err := os.Open("./testdata/ip.data") 45 | if err != nil { 46 | return err 47 | } 48 | defer f.Close() 49 | 50 | scanner := bufio.NewScanner(f) 51 | for scanner.Scan() { 52 | line := scanner.Text() 53 | 54 | item := strings.Split(line, " ") 55 | 56 | start := item[0] 57 | ipint := ipToInt(start) 58 | ipMap[ipint] = IPAddr{ 59 | start: start, 60 | end: item[1], 61 | addr: item[2], 62 | } 63 | ipIndex = append(ipIndex, ipint) 64 | } 65 | return nil 66 | } 67 | 68 | func ipToInt(ip string) int { 69 | ips := strings.Split(ip, ".") 70 | ipInt := 0 71 | var pos uint = 24 72 | for _, ipItem := range ips { 73 | ipint, _ := strconv.Atoi(ipItem) 74 | ipint = ipint << pos 75 | ipInt = ipInt | ipint 76 | pos -= 8 77 | } 78 | return ipInt 79 | } 80 | func init() { 81 | var err error 82 | err = initIPAddress() 83 | if err != nil { 84 | log.Fatalln(err) 85 | } 86 | sort.Ints(ipIndex) 87 | fmt.Println(ipIndex) 88 | } 89 | -------------------------------------------------------------------------------- /algorithm/kmp-string.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | haystack := "abcabdabceabd" 7 | needle := "abceabb" 8 | next := getNext(needle) 9 | fmt.Println(next) 10 | index := kmpSearch(haystack, needle, next) 11 | fmt.Println(index) 12 | } 13 | 14 | func kmpSearch(haystack, needle string, next []int) int { 15 | l1 := len(haystack) 16 | l2 := len(needle) 17 | i, j := 0, 0 18 | for i < l1 && j < l2 { 19 | if j == -1 || haystack[i] == needle[j] { 20 | j++ 21 | i++ 22 | } else { 23 | j = next[j] 24 | } 25 | } 26 | if j == l2 { 27 | return i - j 28 | } 29 | return -1 30 | } 31 | 32 | func getNext(needle string) []int { 33 | next := make([]int, len(needle)) 34 | next[0] = -1 35 | i, j := 0, -1 36 | for i < len(needle)-1 { 37 | if j == -1 || needle[i] == needle[j] { 38 | //真前缀 39 | i += 1 40 | //真后缀 41 | j += 1 42 | next[i] = j 43 | } else { 44 | //真后缀回溯 45 | j = next[j] 46 | } 47 | } 48 | return next 49 | } 50 | -------------------------------------------------------------------------------- /algorithm/largest-subsequence.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | 5 | } 6 | 7 | func maxSubstringSum(arr []int) int { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /algorithm/line.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //输入 1111hhhh333hnn444nn 6 | //输出 7 | //_11111 8 | //hhhh_333 9 | //hnn_444 10 | //nn 11 | func main() { 12 | s := "1111hhhh333hnn444nn" 13 | covert(s) 14 | } 15 | 16 | func covert(s string) { 17 | if s == "" { 18 | return 19 | } 20 | l := len(s) 21 | for i, c := range s { 22 | if c >= '0' && c <= '9' { 23 | if i == 0 { 24 | fmt.Print("_") 25 | } 26 | fmt.Print(string(c)) 27 | if i != l-1 && s[i+1] >= 'a' && s[i+1] <= 'z' { 28 | fmt.Println("") 29 | } 30 | } 31 | if c >= 'a' && c <= 'z' { 32 | fmt.Print(string(c)) 33 | if i != l-1 && s[i+1] >= '0' && s[i+1] <= '9' { 34 | fmt.Print("_") 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /algorithm/longest-common-prefix.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // 编写一个函数查找字符串数组的公共最长前缀,如果不存在则返回空 8 | // 输入 ["flower","flow","flight"] 9 | // 输出 fl 10 | // 输入 【"dog","racecar","car"] 11 | // 输出 "" 12 | func main() { 13 | arr := []string{"fllower", "fllow", "fllight"} 14 | s := getPrefix(arr) 15 | fmt.Println(s) 16 | arr = []string{"dog", "racecar", "car"} 17 | s = getPrefix(arr) 18 | fmt.Println(s) 19 | } 20 | 21 | func getPrefix(arr []string) string { 22 | if len(arr) <= 1 { 23 | return "" 24 | } 25 | firstStr := arr[0] 26 | l := len(arr) 27 | for i := range firstStr { 28 | for j := 1; j < l; j++ { 29 | if arr[j][i] != firstStr[i] { 30 | return firstStr[:i] 31 | } 32 | } 33 | } 34 | return "" 35 | } 36 | -------------------------------------------------------------------------------- /algorithm/longest-substring-without-repeating-characters.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //题目描述 6 | //给定一个字符串,找出最长的不具有重复字符的子串的长度。例如,“abcabcbb”不具有重复字符的最长子串是“abc”,长度为3。对于“bbbbb”,最长的不具有重复字符的子串是“b”,长度为1。 7 | func main() { 8 | s := "abcabcbb" 9 | index := lengthOfLongestSubstring2(s) 10 | fmt.Printf("输出 %d", index) 11 | } 12 | 13 | //双指针法 14 | func lengthOfLongestSubstring(s string) int { 15 | // write code here 16 | win := make(map[uint8]struct{}) 17 | size := len(s) 18 | i := 0 19 | j := 0 20 | maxLen := 0 21 | for i < size && j < size { 22 | if _, ok := win[s[j]]; !ok { 23 | win[s[j]] = struct{}{} 24 | j++ 25 | if m := j - i; m > maxLen { 26 | maxLen = m 27 | } 28 | } else { 29 | delete(win, s[i]) 30 | i++ 31 | } 32 | } 33 | return maxLen 34 | } 35 | 36 | func lengthOfLongestSubstring2(s string) int { 37 | win := make([]int, 256) 38 | size := len(s) 39 | left, right := 0, 0 40 | maxLen := 0 41 | for ; right < size; right++ { 42 | if win[s[right]] > left { 43 | left = win[s[right]] 44 | } 45 | if maxLen < right-left+1 { 46 | maxLen = right - left + 1 47 | } 48 | win[s[right]] = right + 1 49 | } 50 | return maxLen 51 | } 52 | -------------------------------------------------------------------------------- /algorithm/match-sunday-string.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //实现 strStr() 6 | //实现 strStr() 函数。给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。 7 | //示例 1: 8 | // 9 | //输入: haystack = "hello", needle = "ll" 10 | //输出: 2 11 | //示例 2: 12 | // 13 | //输入: haystack = "aaaaa", needle = "bba" 14 | //输出: -1 15 | // https://www.geekxh.com/1.3.%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%B3%BB%E5%88%97/303.html#_02%E3%80%81sunday-%E5%8C%B9%E9%85%8D 16 | func main() { 17 | haystack := "Here is a little Hao" 18 | needle := "little" 19 | index := strStrSunday(haystack, needle) 20 | fmt.Println(index) 21 | } 22 | 23 | //暴力解法 24 | func strStr(haystack, needle string) int { 25 | index := -1 26 | needleIndex := 0 27 | for i := range haystack { 28 | if haystack[i] == needle[needleIndex] { 29 | needleIndex++ 30 | if needleIndex >= len(needle) { 31 | return index 32 | } 33 | if index == -1 { 34 | index = i 35 | } 36 | continue 37 | } 38 | index = -1 39 | needleIndex = 0 40 | } 41 | return index 42 | } 43 | 44 | //使用Sunday算法 45 | func strStrSunday(haystack, needle string) int { 46 | if len(haystack) < len(needle) { 47 | return -1 48 | } 49 | if haystack == needle { 50 | return 0 51 | } 52 | index := -1 53 | i := 0 54 | needleIndex := 0 55 | for i < len(haystack) { 56 | if haystack[i] == needle[needleIndex] { 57 | if index == -1 { 58 | index = i 59 | } 60 | i++ 61 | needleIndex++ 62 | if needleIndex >= len(needle) { 63 | break 64 | } 65 | continue 66 | } 67 | index = -1 68 | i = i + len(needle) - needleIndex 69 | if i >= len(haystack) { 70 | return index 71 | } 72 | offset := 0 73 | for j := len(needle) - 1; j > 0; j-- { 74 | if haystack[i] == needle[j] { 75 | offset = j 76 | break 77 | } 78 | } 79 | 80 | i = i - offset 81 | needleIndex = 0 82 | } 83 | return index 84 | } 85 | -------------------------------------------------------------------------------- /algorithm/multi-array-sort.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | //按照二维数组的第一列升序排序,第二列降序排列 9 | func main() { 10 | nums := [][]int{{1, 2, 3}, {2, 4, 4}, {2, 3, 1}, {1, 3, 1}} 11 | column := 2 12 | fmt.Printf("根据第%d列降序排序:\n", column) 13 | fmt.Println("原数组 ->", nums) 14 | result := sortArray(nums, column, true) 15 | fmt.Println("排序后 ->", result) 16 | } 17 | 18 | func sortArray(arr [][]int, column int, desc bool) [][]int { 19 | intArr := &IntArray{ 20 | arr: arr, 21 | column: column, 22 | desc: desc, 23 | } 24 | sort.Sort(intArr) 25 | return intArr.arr 26 | } 27 | 28 | type IntArray struct { 29 | arr [][]int 30 | column int 31 | desc bool 32 | } 33 | 34 | func (arr *IntArray) Len() int { 35 | return len(arr.arr) 36 | } 37 | func (arr *IntArray) Less(i, j int) bool { 38 | if arr.arr[i][arr.column] < arr.arr[j][arr.column] { 39 | return !arr.desc 40 | } else if arr.arr[i][arr.column] > arr.arr[j][arr.column] { 41 | return arr.desc 42 | } 43 | return true 44 | } 45 | func (arr *IntArray) Swap(i, j int) { 46 | arr.arr[i], arr.arr[j] = arr.arr[j], arr.arr[i] 47 | } 48 | -------------------------------------------------------------------------------- /algorithm/removeNthFromEnd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type ListNode struct { 6 | Value int 7 | Next *ListNode 8 | } 9 | 10 | /** 11 | 题目描述 12 | 给定一个链表,删除链表的倒数第n个节点并返回链表的头指针 13 | 例如, 14 | 给出的链表为:1->2->3->4->5, n= 2. 15 | 删除了链表的倒数第n个节点之后,链表变为1->2->3->5. 16 | 备注: 17 | 题目保证n一定是有效的 18 | 请给出请给出时间复杂度为\ O(n) O(n)的算法 19 | */ 20 | func main() { 21 | l := &ListNode{ 22 | Value: 1, 23 | Next: &ListNode{ 24 | Value: 2, 25 | }, 26 | } 27 | head := removeNthFromEnd(l, 2) 28 | fmt.Printf("%+v", head) 29 | } 30 | 31 | func removeNthFromEnd(head *ListNode, n int) *ListNode { 32 | var pre *ListNode 33 | slow := head 34 | fast := head 35 | for i := n; i > 0; i-- { 36 | if fast == nil { 37 | return nil 38 | } 39 | fast = fast.Next 40 | } 41 | if fast == nil { 42 | return head.Next 43 | } 44 | for fast != nil { 45 | pre = slow 46 | slow = slow.Next 47 | fast = fast.Next 48 | if fast == nil { 49 | pre.Next = slow.Next 50 | } 51 | } 52 | return head 53 | } 54 | 55 | /** 56 | * 57 | * @param pListHead ListNode类 58 | * @param k int整型 59 | * @return ListNode类 60 | */ 61 | func FindKthToTail(pListHead *ListNode, k int) *ListNode { 62 | // write code here 63 | slow := pListHead 64 | fast := pListHead 65 | for i := k; i > 0; i-- { 66 | if fast == nil { 67 | return nil 68 | } 69 | fast = fast.Next 70 | } 71 | for fast != nil { 72 | slow = slow.Next 73 | fast = fast.Next 74 | } 75 | return slow 76 | } 77 | -------------------------------------------------------------------------------- /algorithm/reverse-string.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //反转字符串 6 | //编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 7 | //不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 8 | //示例 1: 9 | // 10 | //输入:["h","e","l","l","o"] 11 | //输出:["o","l","l","e","h"] 12 | //示例 2: 13 | // 14 | //输入:["H","a","n","n","a","h"] 15 | //输出:["h","a","n","n","a","H"] 16 | func main() { 17 | s := []byte{'H', 'a', 'n', 'n', 'a', 'h'} 18 | Reverse(s) 19 | fmt.Printf("%+v", s) 20 | } 21 | 22 | func Reverse(s []byte) { 23 | right := len(s) - 1 24 | left := 0 25 | 26 | for left < right { 27 | s[left], s[right] = s[right], s[left] 28 | left++ 29 | right-- 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /algorithm/reverse_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type ListNode struct { 6 | Value int 7 | Next *ListNode 8 | } 9 | 10 | //翻转链表 11 | func main() { 12 | pHead := &ListNode{ 13 | Value: 1, 14 | Next: nil, 15 | } 16 | head := ReverseList(pHead) 17 | fmt.Println(head) 18 | } 19 | func ReverseList(pHead *ListNode) *ListNode { 20 | var pre *ListNode = nil 21 | var cur = pHead 22 | var nex *ListNode = nil 23 | for cur != nil { 24 | nex = cur.Next 25 | cur.Next = pre 26 | pre = cur 27 | cur = nex 28 | } 29 | return pre 30 | } 31 | -------------------------------------------------------------------------------- /algorithm/sliding-window-maximum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 6 | //返回滑动窗口中的最大值所构成的数组。 7 | //输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 8 | //输出: [3,3,5,5,6,7] 9 | //解释: 10 | // 11 | // 滑动窗口的位置 最大值 12 | //--------------- ----- 13 | //[1 3 -1] -3 5 3 6 7 3 14 | // 1 [3 -1 -3] 5 3 6 7 3 15 | // 1 3 [-1 -3 5] 3 6 7 5 16 | // 1 3 -1 [-3 5 3] 6 7 5 17 | // 1 3 -1 -3 [5 3 6] 7 6 18 | // 1 3 -1 -3 5 [3 6 7] 7 19 | func main() { 20 | arr := []int{1, 3} 21 | fmt.Println(arr[0:1]) 22 | nums := []int{1, 3, -1, -3, 5, 3, 6, 7} 23 | k := 3 24 | ret := maxSlidingWindow2(nums, k) 25 | fmt.Println(ret) 26 | } 27 | 28 | func maxSlidingWindow(nums []int, k int) []int { 29 | l1 := len(nums) 30 | ret := make([]int, 0) 31 | if l1 == 0 || k == 0 { 32 | return ret 33 | } 34 | index := 0 35 | for index < l1 { 36 | m := nums[index] 37 | if index > l1-k { 38 | break 39 | } 40 | for j := index + 1; j < index+k; j++ { 41 | if m < nums[j] { 42 | m = nums[j] 43 | } 44 | } 45 | ret = append(ret, m) 46 | index++ 47 | } 48 | return ret 49 | } 50 | func maxSlidingWindow2(nums []int, k int) []int { 51 | ret := make([]int, 0) 52 | if len(nums) == 0 { 53 | return ret 54 | } 55 | var queue []int 56 | for i := range nums { 57 | for i > 0 && (len(queue) > 0) && nums[i] > queue[len(queue)-1] { 58 | //将比当前元素小的元素祭天 59 | queue = queue[:len(queue)-1] 60 | } 61 | //将当前元素放入queue中 62 | queue = append(queue, nums[i]) 63 | if i >= k && nums[i-k] == queue[0] { 64 | //维护队列,保证其头元素为当前窗口最大值 65 | queue = queue[1:] 66 | } 67 | if i >= k-1 { 68 | //放入结果数组 69 | ret = append(ret, queue[0]) 70 | } 71 | } 72 | return ret 73 | } 74 | -------------------------------------------------------------------------------- /algorithm/sort-colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | //现在有一个包含n个物体的数组,其中物体颜色为颜色为红色、白色或蓝色,请对这个数组进行排序,让相同颜色的物体相邻,颜色的顺序为红色,白色,蓝色。 6 | //我们用0,1,2分别代表颜色红,白,蓝 7 | //注意: 8 | //本题要求你不能使用排序库函数 9 | //扩展: 10 | //一个非常直接的解法是两步的计数排序的算法 11 | //首先:遍历一遍数组,记录0,1,2的数量,然后重写这个数组,先将0写入,再将1写入,再将2写入 12 | //你能给出一个只用一步,并且能在常数级空间复杂度解决这个问题的算法吗? 13 | func main() { 14 | nums := []int{0, 1, 2, 0, 1, 2} 15 | sortColors(nums) 16 | fmt.Println(nums) 17 | } 18 | 19 | func sortColors(arr []int) { 20 | l := len(arr) 21 | if l == 2 { 22 | if arr[0] > arr[1] { 23 | arr[0], arr[1] = arr[1], arr[0] 24 | } 25 | } else if l > 2 { 26 | m := 0 27 | j := l - 1 28 | i := 0 29 | for i < l { 30 | if m > 2 { 31 | break 32 | } 33 | if arr[j] == m { 34 | arr[i], arr[j] = arr[j], arr[i] 35 | i++ 36 | j++ 37 | } 38 | if i == j { 39 | m = m + 1 40 | j = l - 1 41 | continue 42 | } 43 | j-- 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /algorithm/sort/bubble_sort.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | arr := []int{8, 12, 46, 12, 2, 4, 15, 3, 9, 44, 5, 6, 1, 59, 2} 7 | fmt.Println(bubbleSort(arr)) 8 | } 9 | 10 | func bubbleSort(arr []int) []int { 11 | if len(arr) == 0 { 12 | return arr 13 | } 14 | for i := 0; i < len(arr); i++ { 15 | for j := 0; j < len(arr); j++ { 16 | if arr[i] > arr[j] { 17 | arr[j], arr[i] = arr[i], arr[j] 18 | } 19 | } 20 | } 21 | return arr 22 | } 23 | -------------------------------------------------------------------------------- /algorithm/sort/insertion_sort.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | arr := []int{8, 12, 46, 12, 2, 4, 15, 3, 9, 44, 5, 6, 1, 59, 2} 7 | fmt.Println(insertionSort(arr)) 8 | } 9 | 10 | func insertionSort(arr []int) []int { 11 | l := len(arr) 12 | if l == 0 { 13 | return arr 14 | } 15 | for i := 0; i < l-1; i++ { 16 | for j := i + 1; j > 0; j-- { 17 | if arr[j] < arr[j-1] { 18 | arr[j], arr[j-1] = arr[j-1], arr[j] 19 | } 20 | } 21 | } 22 | return arr 23 | } 24 | -------------------------------------------------------------------------------- /algorithm/sort/selection_sort.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | arr := []int{8, 12, 46, 12, 2, 4, 15, 3, 9, 44, 5, 6, 1, 59, 2} 7 | fmt.Println(selectionSort(arr)) 8 | } 9 | 10 | func selectionSort(arr []int) []int { 11 | l := len(arr) 12 | if l == 0 { 13 | return arr 14 | } 15 | for i := 0; i < l; i++ { 16 | min := i 17 | for j := i + 1; j < l; j++ { 18 | if arr[j] < arr[min] { 19 | min = j 20 | } 21 | } 22 | arr[i], arr[min] = arr[min], arr[i] 23 | } 24 | return arr 25 | } 26 | -------------------------------------------------------------------------------- /algorithm/testdata/ip.data: -------------------------------------------------------------------------------- 1 | 172.168.10.1 172.168.10.10 北京市 2 | 172.168.10.11 172.168.10.19 天津市 3 | 172.168.10.20 172.168.10.29 沈阳市 4 | 172.168.10.30 172.168.10.39 吉林市 5 | 172.168.10.40 172.168.10.49 鹤岗市 6 | 172.168.10.50 172.168.10.59 辽源市 7 | 172.168.10.60 172.168.10.69 通辽市 8 | 172.168.10.70 172.168.10.79 鸡西市 9 | 172.168.10.80 172.168.10.89 济南市 10 | 172.168.10.90 172.168.10.99 青岛市 11 | 172.168.10.100 172.168.10.110 东营市 12 | 172.168.10.111 172.168.10.119 威海市 13 | 172.168.10.120 172.168.10.129 郑州市 14 | 172.168.10.130 172.168.10.139 邯郸市 15 | 172.168.10.140 172.168.10.149 西宁市 16 | 172.168.10.150 172.168.10.159 海口市 17 | 172.168.10.160 172.168.10.169 高雄市 18 | 172.168.10.170 172.168.10.179 上海市 19 | 172.168.10.180 172.168.10.189 杭州市 20 | 172.168.10.190 172.168.10.120 余杭市 21 | 172.168.10.200 172.168.10.209 文山州 22 | 172.168.10.210 172.168.10.219 宜春市 23 | 172.168.10.220 172.168.10.229 本溪市 24 | 172.168.10.230 172.168.10.239 铁岭市 25 | 172.168.10.240 172.168.10.249 营口市 26 | 172.168.10.250 172.168.10.254 阳泉市 -------------------------------------------------------------------------------- /algorithm/upper_bound.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | /** 6 | 题目描述 7 | 请实现有重复数字的有序数组的二分查找。 8 | 输出在数组中第一个大于等于查找值的位置,如果数组中不存在这样的数,则输出数组长度加一。 9 | 10 | 示例 11 | 5,4,[1,2,4,4,5] 12 | 13 | 输出:3 14 | */ 15 | func main() { 16 | arr := []int{3, 3, 4, 4, 4, 5, 6, 6, 6, 7, 8, 8, 12, 13, 15, 16, 21, 21, 22, 24, 24, 27, 28, 32, 34, 35, 35, 36, 36, 39, 40, 41, 41, 42, 44, 44, 45, 45, 47, 47, 47, 47, 48, 48, 50, 51, 51, 53, 53, 53, 54, 54, 54, 56, 56, 57, 59, 60, 60, 60, 60, 61, 62, 63, 65, 65, 65, 65, 67, 67, 68, 70, 71, 71, 74, 75, 75, 79, 81, 84, 84, 86, 86, 87, 90, 90, 90, 90, 91, 92, 93, 94, 94, 94, 95, 97, 97, 98, 98, 99} 17 | mid := upper_bound_(100, 97, arr) 18 | fmt.Println(mid) 19 | } 20 | 21 | /** 22 | * 二分查找 23 | * @param n int整型 数组长度 24 | * @param v int整型 查找值 25 | * @param a int整型一维数组 有序数组 26 | * @return int整型 27 | */ 28 | func upper_bound_(n int, v int, a []int) int { 29 | // write code here 30 | left := 0 31 | right := n - 1 32 | for left < right { 33 | mid := left + (right-left)/2 34 | if a[mid] >= v { 35 | if mid == 0 || a[mid-1] < v { 36 | return mid + 1 37 | } 38 | right = mid 39 | } else { 40 | left = mid + 1 41 | } 42 | } 43 | return n + 1 44 | } 45 | -------------------------------------------------------------------------------- /algorithm/verify-palindrome.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func main() { 9 | s := "A man, a plan, a canal: Panama" 10 | fmt.Println(isPalindrome(s)) 11 | s = "race a car" 12 | fmt.Println(isPalindrome(s)) 13 | } 14 | 15 | func isPalindrome(s string) bool { 16 | if s == "" { 17 | return false 18 | } 19 | s = strings.ToLower(s) 20 | if len(s) == 2 { 21 | return s[0] == s[1] 22 | } 23 | left := 0 24 | right := len(s) - 1 25 | for left < right { 26 | if !((s[left] >= 'a' && s[left] <= 'z') || (s[left] >= '0' && s[left] <= '9')) { 27 | left++ 28 | continue 29 | } 30 | if !((s[right] >= 'a' && s[right] <= 'z') || (s[right] >= '0' && s[right] <= '9')) { 31 | right-- 32 | continue 33 | } 34 | if s[left] != s[right] { 35 | return false 36 | } 37 | left++ 38 | right-- 39 | } 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /base/go-gpm.md: -------------------------------------------------------------------------------- 1 | # Go语言的GPM调度器是什么? 2 | 3 | 相信很多人都听说过Go语言天然支持高并发,原因是内部有协程(goroutine)加持,可以在一个进程中启动成千上万个协程。那么,它凭什么做到如此高的并发呢?那就需要先了解什么是并发模型。 4 | 5 | 6 | ## 并发模型 7 | 8 | 著名的C++专家Herb Sutter曾经说过“免费的午餐已经终结”。为了让代码运行的更快,单纯依靠更快的硬件已经无法得到满足,我们需要利用多核来挖掘并行的价值,而并发模型的目的就是来告诉你不同执行实体之间是如何协作的。 9 | 10 | 当然,不同的并发模型的协作方式也不尽相同,常见的并发模型有七种: 11 | 12 | - 线程与锁 13 | - 函数式编程 14 | - Clojure之道 15 | - actor 16 | - 通讯顺序进程(CSP) 17 | - 数据级并行 18 | - Lambda架构 19 | 20 | 而今天,我们只讲与Go语言相关的并发模型CSP,感兴趣的同学可以自行查阅书籍《七周七并发模型》。 21 | 22 | 23 | ### CSP篇 24 | 25 | CSP,全称Communicating Sequential Processes,意为通讯顺序进程,它是七大并发模型中的一种,它的核心观念是将两个并发执行的实体通过通道channel连接起来,所有的消息都通过channel传输。其实CSP概念早在1978年就被东尼·霍尔提出,由于近来Go语言的兴起,CSP又火了起来。 26 | 那么CSP与Go语言有什么关系呢?接下来我们来看Go语言对CSP并发模型的实现——GPM调度模型。 27 | 28 | 29 | ### GPM调度模型 30 | 31 | GPM代表了三个角色,分别是Goroutine、Processor、Machine。 32 | 33 | ![](../images/17188139512ef9a8.jpg) 34 | 35 | - Goroutine:就是咱们常用的用go关键字创建的执行体,它对应一个结构体g,结构体里保存了goroutine的堆栈信息 36 | - Machine:表示操作系统的线程 37 | - Processor:表示处理器,有了它才能建立G、M的联系 38 | 39 | ### Goroutine 40 | 41 | Goroutine就是代码中使用go关键词创建的执行单元,也是大家熟知的有“轻量级线程”之称的协程,协程是不为操作系统所知的,它由编程语言层面实现,上下文切换不需要经过内核态,再加上协程占用的内存空间极小,所以有着非常大的发展潜力。 42 | 43 | ```go 44 | go func() {}() 45 | ``` 46 | 47 | 复制代码在Go语言中,Goroutine由一个名为runtime.go的结构体表示,该结构体非常复杂,有40多个成员变量,主要存储执行栈、状态、当前占用的线程、调度相关的数据。还有玩大家很想获取的goroutine标识,但是很抱歉,官方考虑到Go语言的发展,设置成私有了,不给你调用😏。 48 | 49 | ```go 50 | type g struct { 51 | stack struct { 52 | lo uintptr 53 | hi uintptr 54 | } // 栈内存:[stack.lo, stack.hi) 55 | stackguard0 uintptr 56 | stackguard1 uintptr 57 | 58 | _panic *_panic 59 | _defer *_defer 60 | m *m // 当前的 m 61 | sched gobuf 62 | stktopsp uintptr // 期望 sp 位于栈顶,用于回溯检查 63 | param unsafe.Pointer // wakeup 唤醒时候传递的参数 64 | atomicstatus uint32 65 | goid int64 66 | preempt bool // 抢占信号,stackguard0 = stackpreempt 的副本 67 | timer *timer // 为 time.Sleep 缓存的计时器 68 | 69 | ... 70 | } 71 | ``` 72 | Goroutine调度相关的数据存储在sched,在协程切换、恢复上下文的时候用到。 73 | 74 | ```go 75 | type gobuf struct { 76 | sp uintptr 77 | pc uintptr 78 | g guintptr 79 | ret sys.Uintreg 80 | ... 81 | } 82 | ``` 83 | 84 | M就是对应操作系统的线程,最多会有GOMAXPROCS个活跃线程能够正常运行,默认情况下GOMAXPROCS被设置为内核数,假如有四个内核,那么默认就创建四个线程,每一个线程对应一个runtime.m结构体。线程数等于CPU个数的原因是,每个线程分配到一个CPU上就不至于出现线程的上下文切换,可以保证系统开销降到最低。 85 | 86 | ```go 87 | type m struct { 88 | g0 *g 89 | curg *g 90 | ... 91 | } 92 | ``` 93 | 94 | M里面存了两个比较重要的东西,一个是g0,一个是curg。 95 | 96 | - g0:会深度参与运行时的调度过程,比如goroutine的创建、内存分配等 97 | - curg:代表当前正在线程上执行的goroutine。 98 | 99 | 刚才说P是负责M与G的关联,所以M里面还要存储与P相关的数据。 100 | 101 | ```go 102 | type m struct { 103 | ... 104 | p puintptr 105 | nextp puintptr 106 | oldp puintptr 107 | } 108 | ``` 109 | 110 | - p:正在运行代码的处理器 111 | - nextp:暂存的处理器 112 | - oldp:系统调用之前的线程的处理器 113 | 114 | ### Processor 115 | 116 | Proccessor负责Machine与Goroutine的连接,它能提供线程需要的上下文环境,也能分配G到它应该去的线程上执行,有了它,每个G都能得到合理的调用,每个线程都不再浑水摸鱼,真是居家必备之良品。 117 | 118 | 同样的,处理器的数量也是默认按照GOMAXPROCS来设置的,与线程的数量一一对应。 119 | 120 | ```go 121 | type p struct { 122 | m muintptr 123 | 124 | runqhead uint32 125 | runqtail uint32 126 | runq [256]guintptr 127 | runnext guintptr 128 | ... 129 | } 130 | ``` 131 | 132 | 结构体P中存储了性能追踪、垃圾回收、计时器等相关的字段外,还存储了处理器的待运行队列,队列中存储的是待执行的Goroutine列表。 133 | 134 | ### 三者的关系 135 | 136 | 首先,默认启动四个线程四个处理器,然后互相绑定。 137 | 138 | ![](../images/17188139b47a27ca.jpg) 139 | 140 | 这个时候,一个Goroutine结构体被创建,在进行函数体地址、参数起始地址、参数长度等信息以及调度相关属性更新之后,它就要进到一个处理器的队列等待发车。 141 | 142 | ![](../images/17188139f32b2558.jpg) 143 | 144 | 啥,又创建了一个G?那就轮流往其他P里面放呗,相信你排队取号的时候看到其他窗口没人排队也会过去的。 145 | 146 | ![](../images/1718813a3359a1c0.jpg) 147 | 148 | 假如有很多G,都塞满了怎么办呢?那就不把G塞到处理器的私有队列里了,而是把它塞到全局队列里(候车大厅)。 149 | 150 | ![](../images/1718813a71f700a1.jpg) 151 | 152 | 除了往里塞之外,M这边还要疯狂往外取,首先去处理器的私有队列里取G执行,如果取完的话就去全局队列取,如果全局队列里也没有的话,就去其他处理器队列里偷,哇,这么饥渴,简直是恶魔啊! 153 | 154 | ![](../images/1718813abb679031.jpg) 155 | 156 | 如果哪里都没找到要执行的G呢?那M就会因为太失望和P断开关系,然后去睡觉(idle)了。 157 | 158 | ![](../images/1718813afa26977e.jpg) 159 | 160 | 那如果两个Goroutine正在通过channel做一些恩恩爱爱的事阻塞住了怎么办,难道M要等他们完事了再继续执行?显然不会,M并不稀罕这对Go男女,而会转身去找别的G执行。 161 | 162 | ![](../images/1718813b326b7c73.jpg) 163 | 164 | 165 | ## 系统调用 166 | 167 | 如果G进行了系统调用syscall,M也会跟着进入系统调用状态,那么这个P留在这里就浪费了,怎么办呢?这点精妙之处在于,P不会傻傻的等待G和M系统调用完成,而会去找其他比较闲的M执行其他的G。 168 | 169 | ![](../images/1718813b73c47064.jpg) 170 | 171 | 当G完成了系统调用,因为要继续往下执行,所以必须要再找一个空闲的处理器发车。 172 | 173 | ![](../images/1718813bad18d998.jpg) 174 | 175 | 如果没有空闲的处理器了,那就只能把G放回全局队列当中等待分配。 176 | 177 | ![](../images/1718813bec3d6674.jpg) 178 | 179 | 180 | ## sysmon 181 | 182 | sysmon是我们的保洁阿姨,它是一个M,又叫监控线程,不需要P就可以独立运行,每20us~10ms会被唤醒一次出来打扫卫生,主要工作就是回收垃圾、回收长时间系统调度阻塞的P、向长时间运行的G发出抢占调度等等。 183 | 184 | > 作者:平也 185 | > 链接:https://juejin.im/post/5e999ead518825739b2d44d7 186 | > 来源:掘金 187 | > 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /base/go-grammar.md: -------------------------------------------------------------------------------- 1 | # Golang常见语法面试题总结 2 | 3 | # 一 在go语言中,new和make的区别? 4 | 5 | new 的作用是初始化一个指向类型的指针(*T) 6 | 7 | new函数是内建函数,函数定义:`func new(Type) *Type` 8 | 9 | 使用new函数来分配空间。传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。 10 | 11 | make 的作用是为 slice,map 或 chan 初始化并返回引用(T)。 12 | 13 | make函数是内建函数,函数定义:func make(Type, size IntegerType) Type 14 | 15 | - 第一个参数是一个类型,第二个参数是长度 16 | - 返回值是一个类型 17 | 18 | `make(T, args)`函数的目的与new(T)不同。它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是T*)的一个初始化的(不是零值)的实例。 19 | 20 | ## 二 在go语言中,Printf()、Sprintf()、Fprintf()函数的区别用法是什么? 21 | 22 | 都是把格式好的字符串输出,只是输出的目标不一样: 23 | Printf(),是把格式字符串输出到标准输出(一般是屏幕,可以重定向)。 24 | Printf() 是和标准输出文件(stdout)关联的,Fprintf 则没有这个限制. 25 | Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。 26 | Fprintf(), 是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*。主要用于文件操作。Fprintf()是格式化输出到一个stream,通常是到文件。 27 | ## 三、 说说go语言中,数组与切片的区别? 28 | - (1). 数组 数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。 数组的长度是数组类型的一部分,所以[3]int 和 [4]int 是两种不同的数组类型。 29 | 30 | - 数组需要指定大小,不指定也会根据初始化的自动推算出大小,不可改变 ; 31 | 32 | - 数组是值传递; 33 | 34 | - 数组是内置(build-in)类型,是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。 35 | 36 | - 数组定义: 37 | 38 | 1. var array [10]int 39 | 40 | 2. var array = [5]int{1,2,3,4,5} 41 | 42 | - (2). 切片 切片表示一个拥有相同类型元素的可变长度的序列。 切片是一种轻量级的数据结构,它有三个属性:指针、长度和容量。 43 | 44 | - 切片不需要指定大小; 45 | 46 | - 切片是地址传递; 47 | 48 | - 切片可以通过数组来初始化,也可以通过内置函数make()初始化 .初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容; 49 | 50 | - 切片定义: 51 | 52 | 1. var slice []type = make([]type, len) 53 | 54 | 55 | ## 四、 解释以下命令的作用? 56 | 57 | - go env: #用于查看go的环境变量 58 | - go run: #用于编译并运行go源码文件 59 | - go build: #用于编译源码文件、代码包、依赖包 60 | - go get: #用于动态获取远程代码包 61 | - go install: #用于编译go文件,并将编译结构安装到bin、pkg目录 62 | - go clean: #用于清理工作目录,删除编译和安装遗留的目标文件 63 | - go version: #用于查看go的版本信息 64 | 65 | ## 五、 说说go语言中的协程? 66 | 67 | 协程和线程都可以实现程序的并发执行; 68 | 69 | 通过channel来进行协程间的通信; 70 | 71 | 只需要在函数调用前添加go关键字即可实现go的协程,创建并发任务; 72 | 73 | 关键字go并非执行并发任务,而是创建一个并发任务单元; 74 | 75 | ## 六、 go语言中没有隐藏的this指针,这句话是什么意思? 76 | 77 | 方法施加的对象显式传递,没有被隐藏起来 78 | 79 | golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达 80 | 81 | 方法施加的对象不需要非得是指针,也不用非得叫this 82 | 83 | ## 七、 go语言中的引用类型包含哪些? 84 | 85 | 数组切片、字典(map)、通道(channel)、接口(interface) 86 | 87 | 1. **切片(Slice):** 88 | 89 | 切片是对数组的引用,具有动态大小。切片包含一个指向底层数组的指针、切片的长度以及容量。 90 | 91 | 切片在传递给函数或赋值时,传递的是对底层数组的引用,而不是数组的副本。 92 | 93 | 2. **字典(Map):** 94 | 95 | 字典是一种键值对的数据结构,类似于其他语言中的哈希表或字典。 96 | 97 | Map的底层实现是引用类型,因此在函数间传递时,是通过引用传递的。 98 | 99 | 3. **通道(Channel):** 100 | 101 | 通道用于在Goroutine之间传递数据。 102 | 103 | 通道本身是引用类型,传递的是对通道的引用。 104 | 105 | 4. **指针(Pointer):** 106 | 107 | 指针是对变量地址的引用。 108 | 109 | 通过指针可以间接地访问或修改变量的值。 110 | 111 | 5. **接口(Interface):** 112 | 113 | 接口定义了一组方法的集合。 114 | 115 | 接口变量实际上是一个包含了类型信息和类型值的双重指针结构,因此在赋值或传递时也是引用类型的行为。 116 | 117 | ## 八、 说说go语言的同步锁? 118 | 119 | - (1) 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex 120 | 121 | - (2) RWMutex在读锁占用的情况下,会阻止写,但不阻止读 122 | 123 | - (3) RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占 124 | 125 | ## 九、 说说go语言的channel特性? 126 | 127 | - **通信与同步机制** 128 | 129 | - **安全的数据传输:** channel 是 Go 语言中用于在不同 goroutine 之间进行通信和同步的机制,它提供了一种安全、高效的方式来传递数据,避免了并发访问数据时的竞态条件和死锁等问题,确保数据的完整性和一致性。 130 | 131 | - **同步等待:** 无缓冲 channel 在发送和接收操作时会阻塞当前 goroutine,直到对应的接收或发送操作准备好,从而实现了数据的同步等待,保证数据在发送和接收时的原子性和顺序性,数据的发送和接收是一对一的同步关系,先发送的数据会先被接收。 132 | 133 | - **类型安全** 134 | 135 | channel 只能传递指定类型的数据,在创建 channel 时需要指定其元素类型,如chan int表示只能传递整数类型数据的 channel,chan string只能传递字符串类型数据,这使得在编译阶段就能发现类型不匹配的错误,增强了程序的可靠性。 136 | 137 | - **阻塞特性** 138 | 139 | - **发送阻塞:** 对于无缓冲 channel,当发送数据时,如果没有对应的接收者准备好接收数据,发送操作会被阻塞;对于缓冲 channel,当缓冲区已满时,发送操作也会被阻塞,直到缓冲区有空闲空间。 140 | 141 | - **接收阻塞:** 无缓冲 channel 在没有数据可接收时,接收操作会被阻塞;缓冲 channel 在缓冲区为空时,接收操作会被阻塞,直到有数据可供接收。 142 | 143 | - **缓冲功能** 144 | 145 | - **缓冲机制:** 可以创建带缓冲的 channel,它可以在一定程度上存储数据,在发送端和接收端之间暂存一定数量的数据,从而减少阻塞,提高并发处理的效率。定义时通过指定第二个参数来设置缓冲区大小,如ch := make(chan int, 10)表示创建一个可以缓存 10 个整数的 channel。 146 | 147 | - **非阻塞发送与接收:** 在缓冲 channel 未满时,发送操作可以立即完成而不会阻塞;在缓冲 channel 未空时,接收操作也可以立即完成。 148 | 149 | - **方向限制** 150 | 151 | 可以限定 channel 只能进行发送或接收操作,从而提高代码的可读性和安全性。通过在定义 channel 类型时使用chan<-表示只能发送的单向 channel,<-chan表示只能接收的单向 channel,如func senddata(ch chan<- int)表示该函数的参数 ch 是一个只能发送整数的 channel。 152 | 153 | - **可关闭性** 154 | 155 | 发送者可以关闭一个 channel 来表示没有更多的值会被发送,接收者可以通过额外的接收参数来检查 channel 是否已经关闭,如v, ok := <-ch,如果ok为true,则表示v是从 channel 中接收的值,如果ok为false,则表示 channel 已经关闭,并且没有更多的值可接收。 156 | 157 | - **支持多路复用** 158 | 159 | 可以使用select语句同时监听多个 channel,一旦某个 channel 准备好进行数据交换,就会执行对应的操作,从而更加灵活地处理并发任务,实现对多个 channel 的并发处理和协调。 160 | 161 | ## 十、 说说go语言的select机制? 162 | 163 | **主要特性** 164 | 165 | - **多路复用:** 166 | 167 | select允许一个Goroutine等待多个通道操作。只要其中一个通道操作完成,select就可以继续进行。 168 | 169 | - **随机选择:** 170 | 171 | 如果有多个通道操作同时就绪,select会随机选择一个执行。这种随机选择机制有助于避免通道操作的偏向性。 172 | 173 | - **阻塞行为:** 174 | 175 | 如果所有通道都没有准备好,select将阻塞,直到有一个通道可以进行操作。 176 | 177 | 可以通过在select中使用default子句来避免阻塞,此时select会立即执行default中的代码块。 178 | 179 | - **超时处理:** 180 | 181 | 通过select与time.After结合,可以实现超时处理。例如,等待某个操作不超过指定的时间。 182 | 183 | **使用场景** 184 | 185 | - **同时等待多个通道操作:** 186 | 187 | select可以等待多个通道的读写操作,这在需要从多个信号源接收数据时特别有用。 188 | 189 | - **实现超时功能:** 190 | 191 | 通过select和time.After,可以在一定时间内等待通道操作,否则执行超时逻辑。 192 | 193 | - **非阻塞通信:** 194 | 195 | 使用select的default分支,可以实现非阻塞的通道操作。 196 | 197 | - **处理多个Goroutine的结果:** 198 | 199 | 在需要从多个并发任务中收集结果时,select可以帮助从多个通道中读取数据。 200 | 201 | ## 十一、说说进程、线程、协程之间的区别? 202 | 203 | **概念** 204 | 205 | - **进程:** 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 206 | 207 | - **线程:** 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。 208 | 209 | - **协程:** 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 210 | 211 | **区别** 212 | 213 | **进程与线程比较** 214 | 215 | - 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间 216 | 217 | - 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源 218 | 219 | - 线程是处理器调度的基本单位,但进程不是 220 | 221 | - 二者均可并发执行 222 | 223 | - 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 224 | 225 | **协程与线程进行比较** 226 | 227 | 1. 一个线程可以多个协程,一个进程也可以单独拥有多个协程 228 | 229 | 2. 线程进程都是同步机制,而协程则是异步 230 | 231 | 3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态 -------------------------------------------------------------------------------- /base/go-scheduler.md: -------------------------------------------------------------------------------- 1 | # Goroutine调度策略 2 | 3 | > 原文: [第三章 Goroutine调度策略(16)](https://mp.weixin.qq.com/s?__biz=MzU1OTg5NDkzOA==&mid=2247483801&idx=1&sn=ef7f872afccf148661cbd5a3d3b5b0a2&scene=19#wechat_redirect) 4 | 5 | 6 | 在调度器概述一节我们提到过,所谓的goroutine调度,是指程序代码按照一定的算法在适当的时候挑选出合适的goroutine并放到CPU上去运行的过程。这句话揭示了调度系统需要解决的三大核心问题: 7 | 8 | - 调度时机:什么时候会发生调度? 9 | - 调度策略:使用什么策略来挑选下一个进入运行的goroutine? 10 | - 切换机制:如何把挑选出来的goroutine放到CPU上运行? 11 | 12 | 对这三大问题的解决构成了调度器的所有工作,因而我们对调度器的分析也必将围绕着它们所展开。 13 | 14 | 第二章我们已经详细的分析了调度器的初始化以及goroutine的切换机制,本章将重点讨论调度器如何挑选下一个goroutine出来运行的策略问题,而剩下的与调度时机相关的内容我们将在第4~6章进行全面的分析。 15 | 16 | ## 再探schedule函数 17 | 18 | 在讨论main goroutine的调度时我们已经见过schedule函数,因为当时我们的主要关注点在于main goroutine是如何被调度到CPU上运行的,所以并未对schedule函数如何挑选下一个goroutine出来运行做深入的分析,现在是重新回到schedule函数详细分析其调度策略的时候了。 19 | 20 | [runtime/proc.go : 2467](https://github.com/golang/go/blob/bcda68447b31b86bc3829fca80454ca1a2a572e0/src/runtime/proc.go#L2551) 21 | 22 | ```go 23 | // One round of scheduler: find a runnable goroutine and execute it. 24 | // Never returns. 25 | func schedule() { 26 | _g_ := getg() //_g_ = m.g0 27 | 28 | ...... 29 | 30 | var gp *g 31 | 32 | ...... 33 | 34 | if gp == nil { 35 | // Check the global runnable queue once in a while to ensure fairness. 36 | // Otherwise two goroutines can completely occupy the local runqueue 37 | // by constantly respawning each other. 38 | //为了保证调度的公平性,每个工作线程每进行61次调度就需要优先从全局运行队列中获取goroutine出来运行, 39 | //因为如果只调度本地运行队列中的goroutine,则全局运行队列中的goroutine有可能得不到运行 40 | if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { 41 | lock(&sched.lock) //所有工作线程都能访问全局运行队列,所以需要加锁 42 | gp = globrunqget(_g_.m.p.ptr(), 1) //从全局运行队列中获取1个goroutine 43 | unlock(&sched.lock) 44 | } 45 | } 46 | if gp == nil { 47 | //从与m关联的p的本地运行队列中获取goroutine 48 | gp, inheritTime = runqget(_g_.m.p.ptr()) 49 | if gp != nil && _g_.m.spinning { 50 | throw("schedule: spinning with local work") 51 | } 52 | } 53 | if gp == nil { 54 | //如果从本地运行队列和全局运行队列都没有找到需要运行的goroutine, 55 | //则调用findrunnable函数从其它工作线程的运行队列中偷取,如果偷取不到,则当前工作线程进入睡眠, 56 | //直到获取到需要运行的goroutine之后findrunnable函数才会返回。 57 | gp, inheritTime = findrunnable() // blocks until work is available 58 | } 59 | 60 | ...... 61 | 62 | //当前运行的是runtime的代码,函数调用栈使用的是g0的栈空间 63 | //调用execte切换到gp的代码和栈空间去运行 64 | execute(gp, inheritTime) 65 | } 66 | ``` 67 | 68 | schedule函数分三步分别从各运行队列中寻找可运行的goroutine: 69 | 70 | - 第一步,从全局运行队列中寻找goroutine。为了保证调度的公平性,每个工作线程每经过61次调度就需要优先尝试从全局运行队列中找出一个goroutine来运行,这样才能保证位于全局运行队列中的goroutine得到调度的机会。全局运行队列是所有工作线程都可以访问的,所以在访问它之前需要加锁。 71 | 72 | - 第二步,从工作线程本地运行队列中寻找goroutine。如果不需要或不能从全局运行队列中获取到goroutine则从本地运行队列中获取。 73 | 74 | - 第三步,从其它工作线程的运行队列中偷取goroutine。如果上一步也没有找到需要运行的goroutine,则调用findrunnable从其他工作线程的运行队列中偷取goroutine,findrunnable函数在偷取之前会再次尝试从全局运行队列和当前线程的本地运行队列中查找需要运行的goroutine。 75 | 76 | 下面我们先来看如何从全局运行队列中获取goroutine。 77 | 78 | ## 从全局运行队列中获取goroutine 79 | 80 | 从全局运行队列中获取可运行的goroutine是通过globrunqget函数来完成的,该函数的第一个参数是与当前工作线程绑定的p,第二个参数max表示最多可以从全局队列中拿多少个g到当前工作线程的本地运行队列中来。 81 | 82 | [runtime/proc.go : 4663](https://github.com/golang/go/blob/bcda68447b31b86bc3829fca80454ca1a2a572e0/src/runtime/proc.go#L4996) 83 | 84 | ```go 85 | // Try get a batch of G's from the global runnable queue. 86 | // Sched must be locked. 87 | func globrunqget(_p_ *p, max int32) *g { 88 | if sched.runqsize == 0 { //全局运行队列为空 89 | return nil 90 | } 91 | 92 | //根据p的数量平分全局运行队列中的goroutines 93 | n := sched.runqsize / gomaxprocs + 1 94 | if n > sched.runqsize { //上面计算n的方法可能导致n大于全局运行队列中的goroutine数量 95 | n = sched.runqsize 96 | } 97 | if max > 0 && n > max { 98 | n = max //最多取max个goroutine 99 | } 100 | if n > int32(len(_p_.runq)) / 2 { 101 | n = int32(len(_p_.runq)) / 2 //最多只能取本地队列容量的一半 102 | } 103 | 104 | sched.runqsize -= n 105 | 106 | //直接通过函数返回gp,其它的goroutines通过runqput放入本地运行队列 107 | gp := sched.runq.pop() //pop从全局运行队列的队列头取 108 | n-- 109 | for ; n > 0; n-- { 110 | gp1 := sched.runq.pop() //从全局运行队列中取出一个goroutine 111 | runqput(_p_, gp1, false) //放入本地运行队列 112 | } 113 | return gp 114 | } 115 | ``` 116 | 117 | globrunqget函数首先会根据全局运行队列中goroutine的数量,函数参数max以及_p_的本地队列的容量计算出到底应该拿多少个goroutine,然后把第一个g结构体对象通过返回值的方式返回给调用函数,其它的则通过runqput函数放入当前工作线程的本地运行队列。这段代码值得一提的是,计算应该从全局运行队列中拿走多少个goroutine时根据p的数量(gomaxprocs)做了负载均衡。 118 | 119 | 如果没有从全局运行队列中获取到goroutine,那么接下来就在工作线程的本地运行队列中寻找需要运行的goroutine。 120 | 121 | ## 从工作线程本地运行队列中获取goroutine 122 | 123 | 从代码上来看,工作线程的本地运行队列其实分为两个部分,一部分是由p的runq、runqhead和runqtail这三个成员组成的一个无锁循环队列,该队列最多可包含256个goroutine;另一部分是p的runnext成员,它是一个指向g结构体对象的指针,它最多只包含一个goroutine。 124 | 125 | 从本地运行队列中寻找goroutine是通过`runqget`函数完成的,寻找时,代码首先查看`runnext`成员是否为空,如果不为空则返回runnext所指的goroutine,并把runnext成员清零,如果runnext为空,则继续从循环队列中查找goroutine。 126 | 127 | [runtime/proc.go : 4825](https://github.com/golang/go/blob/bcda68447b31b86bc3829fca80454ca1a2a572e0/src/runtime/proc.go#L5192:1) 128 | 129 | ```go 130 | // Get g from local runnable queue. 131 | // If inheritTime is true, gp should inherit the remaining time in the 132 | // current time slice. Otherwise, it should start a new time slice. 133 | // Executed only by the owner P. 134 | func runqget(_p_ *p) (gp *g, inheritTime bool) { 135 | // If there's a runnext, it's the next G to run. 136 | //从runnext成员中获取goroutine 137 | for { 138 | //查看runnext成员是否为空,不为空则返回该goroutine 139 | next := _p_.runnext 140 | if next == 0 { 141 | break 142 | } 143 | if _p_.runnext.cas(next, 0) { 144 | return next.ptr(), true 145 | } 146 | } 147 | 148 | //从循环队列中获取goroutine 149 | for { 150 | h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers 151 | t := _p_.runqtail 152 | if t == h { 153 | return nil, false 154 | } 155 | gp := _p_.runq[h%uint32(len(_p_.runq))].ptr() 156 | if atomic.CasRel(&_p_.runqhead, h, h+1) { // cas-release, commits consume 157 | return gp, false 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | 这里首先需要注意的是不管是从runnext还是从循环队列中拿取goroutine都使用了cas操作,这里的cas操作是必需的,因为可能有其他工作线程此时此刻也正在访问这两个成员,从这里偷取可运行的goroutine。 164 | 165 | 其次,代码中对runqhead的操作使用了`atomic.LoadAcq`和`atomic.CasRel`,它们分别提供了`load-acquire`和`cas-release`语义。 166 | 167 | **对于atomic.LoadAcq来说,其语义主要包含如下几条:** 168 | 169 | - 原子读取,也就是说不管代码运行在哪种平台,保证在读取过程中不会有其它线程对该变量进行写入; 170 | - 位于`atomic.LoadAcq`之后的代码,对内存的读取和写入必须在`atomic.LoadAcq`读取完成后才能执行,编译器和CPU都不能打乱这个顺序; 171 | - 当前线程执行`atomic.LoadAcq`时可以读取到其它线程最近一次通过`atomic.CasRel`对同一个变量写入的值,与此同时,位于`atomic.LoadAcq`之后的代码,不管读取哪个内存地址中的值,都可以读取到其它线程中位于atomic.CasRel(对同一个变量操作)之前的代码最近一次对内存的写入。 172 | 173 | **对于atomic.CasRel来说,其语义主要包含如下几条:** 174 | 175 | - 原子的执行比较并交换的操作; 176 | - 位于`atomic.CasRel`之前的代码,对内存的读取和写入必须在`atomic.CasRel`对内存的写入之前完成,编译器和CPU都不能打乱这个顺序; 177 | - 线程执行`atomic.CasRel`完成后其它线程通过`atomic.LoadAcq`读取同一个变量可以读到最新的值,与此同时,位于`atomic.CasRel`之前的代码对内存写入的值,可以被其它线程中位于`atomic.LoadAcq`(对同一个变量操作)之后的代码读取到。 178 | 179 | 因为可能有多个线程会并发的修改和读取`runqhead`,以及需要依靠runqhead的值来读取runq数组的元素,所以需要使用atomic.LoadAcq和atomic.CasRel来保证上述语义。 180 | 181 | 我们可能会问,为什么读取p的runqtail成员不需要使用atomic.LoadAcq或atomic.load?因为runqtail不会被其它线程修改,只会被当前工作线程修改,此时没有人修改它,所以也就不需要使用原子相关的操作。 182 | 183 | 最后,由`p`的`runq`、`runqhead`和`runqtail`这三个成员组成的这个无锁循环队列非常精妙,我们会在后面的章节对这个循环队列进行分析。 184 | 185 | ## CAS操作与ABA问题 186 | 187 | 我们知道使用cas操作需要特别注意ABA的问题,那么runqget函数这两个使用cas的地方会不会有问题呢?答案是这两个地方都不会有ABA的问题。原因分析如下: 188 | 189 | 首先来看对runnext的cas操作。只有跟_p_绑定的当前工作线程才会去修改runnext为一个非0值,其它线程只会把runnext的值从一个非0值修改为0值,然而跟_p_绑定的当前工作线程正在此处执行代码,所以在当前工作线程读取到值A之后,不可能有线程修改其值为B(0)之后再修改回A。 190 | 191 | 再来看对runq的cas操作。当前工作线程操作的是_p_的本地队列,只有跟_p_绑定在一起的当前工作线程才会因为往该队列里面添加goroutine而去修改runqtail,而其它工作线程不会往该队列里面添加goroutine,也就不会去修改runqtail,它们只会修改runqhead,所以,当我们这个工作线程从runqhead读取到值A之后,其它工作线程也就不可能修改runqhead的值为B之后再第二次把它修改为值A(因为runqtail在这段时间之内不可能被修改,runqhead的值也就无法越过runqtail再回绕到A值),也就是说,代码从逻辑上已经杜绝了引发ABA的条件。 192 | 193 | 到此,我们已经分析完工作线程从全局运行队列和本地运行队列获取goroutine的代码,由于篇幅的限制,我们下一节再来分析从其它工作线程的运行队列偷取goroutine的流程。 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /base/redis-rdb.md: -------------------------------------------------------------------------------- 1 | # Redis持久化的原理及优化 2 | 3 | Redis为持久化提供了两种方式: 4 | 5 | - RDB:在指定的时间间隔能对你的数据进行快照存储。 6 | - AOF:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据。 7 | 8 | 本文将通过下面内容的介绍,希望能够让大家更全面、清晰的认识这两种持久化方式,同时理解这种保存数据的思路,应用于自己的系统设计中。 9 | 10 | - 持久化的配置 11 | - RDB与AOF持久化的工作原理 12 | - 如何从持久化中恢复数据 13 | - 关于性能与实践建议 14 | 15 | ## 持久化的配置 16 | 17 | 为了使用持久化的功能,我们需要先知道该如何开启持久化的功能。 18 | 19 | ### RDB的持久化配置 20 | 21 | ```bash 22 | # 时间策略 23 | save 900 1 24 | save 300 10 25 | save 60 10000 26 | 27 | # 文件名称 28 | dbfilename dump.rdb 29 | 30 | # 文件保存路径 31 | dir /home/work/app/redis/data/ 32 | 33 | # 如果持久化出错,主进程是否停止写入 34 | stop-writes-on-bgsave-error yes 35 | 36 | # 是否压缩 37 | rdbcompression yes 38 | 39 | # 导入时是否检查 40 | rdbchecksum yes 41 | ``` 42 | 43 | 配置其实非常简单,这里说一下持久化的时间策略具体是什么意思。 44 | 45 | - save 900 1 表示900s内如果有1条是写入命令,就触发产生一次快照,可以理解为就进行一次备份 46 | - save 300 10 表示300s内有10条写入,就产生快照 47 | 48 | 下面的类似,那么为什么需要配置这么多条规则呢?因为Redis每个时段的读写请求肯定不是均衡的,为了平衡性能与数据安全,我们可以自由定制什么情况下触发备份。所以这里就是根据自身Redis写入情况来进行合理配置。 49 | 50 | `stop-writes-on-bgsave-error yes` 这个配置也是非常重要的一项配置,这是当备份进程出错时,主进程就停止接受新的写入操作,是为了保护持久化的数据一致性问题。如果自己的业务有完善的监控系统,可以禁止此项配置, 否则请开启。 51 | 52 | 关于压缩的配置 `rdbcompression yes` ,建议没有必要开启,毕竟Redis本身就属于CPU密集型服务器,再开启压缩会带来更多的CPU消耗,相比硬盘成本,CPU更值钱。 53 | 54 | 当然如果你想要禁用RDB配置,也是非常容易的,只需要在save的最后一行写上:`save ""` 55 | 56 | ### AOF的配置 57 | 58 | ```bash 59 | 60 | # 是否开启aof 61 | appendonly yes 62 | 63 | # 文件名称 64 | appendfilename "appendonly.aof" 65 | 66 | # 同步方式 67 | appendfsync everysec 68 | 69 | # aof重写期间是否同步 70 | no-appendfsync-on-rewrite no 71 | 72 | # 重写触发配置 73 | auto-aof-rewrite-percentage 100 74 | auto-aof-rewrite-min-size 64mb 75 | 76 | # 加载aof时如果有错如何处理 77 | aof-load-truncated yes 78 | 79 | # 文件重写策略 80 | aof-rewrite-incremental-fsync yes 81 | ``` 82 | 83 | 复制代码还是重点解释一些关键的配置: 84 | 85 | `appendfsync everysec` 它其实有三种模式: 86 | 87 | - always:把每个写命令都立即同步到aof,很慢,但是很安全 88 | - everysec:每秒同步一次,是折中方案 89 | - no:redis不处理交给OS来处理,非常快,但是也最不安全 90 | 91 | 一般情况下都采用 `everysec` 配置,这样可以兼顾速度与安全,最多损失1s的数据。 92 | 93 | `aof-load-truncated yes` 如果该配置启用,在加载时发现aof尾部不正确是,会向客户端写入一个log,但是会继续执行,如果设置为 `no` ,发现错误就会停止,必须修复后才能重新加载。 94 | 95 | ## 工作原理 96 | 97 | 关于原理部分,我们主要来看RDB与AOF是如何完成持久化的,他们的过程是如何。 98 | 99 | 在介绍原理之前先说下Redis内部的定时任务机制,定时任务执行的频率可以在配置文件中通过 hz 10 来设置(这个配置表示1s内执行10次,也就是每100ms触发一次定时任务)。该值最大能够设置为:500,但是不建议超过:100,因为值越大说明执行频率越频繁越高,这会带来CPU的更多消耗,从而影响主进程读写性能。 100 | 101 | 定时任务使用的是Redis自己实现的 TimeEvent,它会定时去调用一些命令完成定时任务,这些任务可能会阻塞主进程导致Redis性能下降。因此我们在配置Redis时,一定要整体考虑一些会触发定时任务的配置,根据实际情况进行调整。 102 | 103 | ### RDB的原理 104 | 105 | 在Redis中RDB持久化的触发分为两种:自己手动触发与Redis定时触发。 106 | 107 | **针对RDB方式的持久化,手动触发可以使用:** 108 | 109 | - save:会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。 110 | - bgsave:该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。 111 | 112 | **而自动触发的场景主要是有以下几点:** 113 | 114 | - 根据我们的 `save m n` 配置规则自动触发; 115 | - 从节点全量复制时,主节点发送`rdb`文件给从节点完成复制操作,主节点会触发 `bgsave`; 116 | - 执行 debug reload 时; 117 | - 执行 shutdown时,如果没有开启aof,也会触发。 118 | 119 | 由于 save 基本不会被使用到,我们重点看看 bgsave 这个命令是如何完成RDB的持久化的。 120 | 121 | ![](../images/16530eac18882d66.jpg) 122 | 123 | 这里注意的是 fork 操作会阻塞,导致Redis读写性能下降。我们可以控制单个Redis实例的最大内存,来尽可能降低Redis在fork时的事件消耗。以及上面提到的自动触发的频率减少fork次数,或者使用手动触发,根据自己的机制来完成持久化。 124 | 125 | ### AOF的原理 126 | 127 | AOF的整个流程大体来看可以分为两步,一步是命令的实时写入(如果是 `appendfsync everysec` 配置,会有`1s`损耗),第二步是对aof文件的重写。 128 | 对于增量追加到文件这一步主要的流程是:`命令写入=>追加到aof_buf =>同步到aof磁盘`。那么这里为什么要先写入buf在同步到磁盘呢?如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。 129 | aof重写是为了减少aof文件的大小,可以手动或者自动触发,关于自动触发的规则请看上面配置部分。fork的操作也是发生在重写这一步,也是这里会对主进程产生阻塞。 130 | 手动触发: `bgrewriteaof`,自动触发 就是根据配置规则来触发,当然自动触发的整体时间还跟Redis的定时任务频率有关系。 131 | 132 | 下面来看看重写的一个流程图: 133 | 134 | ![](../images/16530eac181d94c8.jpg) 135 | 136 | 对于上图有四个关键点补充一下: 137 | 138 | 在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;因此它依然会写入旧的AOF file中,如果重写失败,能够保证数据不丢失。 139 | 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个buf,防止新写的file丢失数据。 140 | 重写是直接把当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。 141 | AOF文件直接采用的文本协议,主要是兼容性好、追加方便、可读性高可认为修改修复。 142 | 143 | 144 | > 不能是RDB还是AOF都是先写入一个临时文件,然后通过 rename 完成文件的替换工作。 145 | 146 | ## 从持久化中恢复数据 147 | 148 | 数据的备份、持久化做完了,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢? 149 | 其实想要从这些文件中恢复数据,只需要重新启动Redis即可。我们还是通过图来了解这个流程: 150 | 151 | ![](../images/16530eac182b1b66.jpg) 152 | 153 | 154 | 启动时会先检查AOF文件是否存在,如果不存在就尝试加载RDB。那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。 155 | 156 | ## 性能与实践 157 | 158 | 通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。 159 | 160 | - 降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写; 161 | - 控制Redis最大使用内存,防止fork耗时过长; 162 | - 使用更牛逼的硬件; 163 | - 合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。 164 | 165 | 在线上我们到底该怎么做?我提供一些自己的实践经验。 166 | 167 | - 如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回; 168 | - 自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据; 169 | - 单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行; 170 | - 可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令; 171 | - RDB持久化与AOF持久化可以同时存在,配合使用。 172 | 173 | 174 | > 作者:大愚Talk 175 | > 链接:https://juejin.im/post/5b70dfcf518825610f1f5c16 176 | > 来源:掘金 177 | > 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 -------------------------------------------------------------------------------- /base/redis.md: -------------------------------------------------------------------------------- 1 | # Redis 基础 2 | 3 | ## Redis常见的数据结构? 4 | 5 | String、Hash、List、Set、SortedSet。 6 | 7 | ### 1.String 字符串类型 8 | 9 | 是redis中最基本的数据类型,一个key对应一个value。 10 | 11 | String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。 12 | 13 | **实战场景:** 14 | 15 | 1. 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。 16 | 2. 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。 17 | 3. session:常见方案spring session + redis实现session共享 18 | 19 | ### 2.Hash (哈希) 20 | 21 | 是一个Mapmap,指值本身又是一种键值对结构,如 value={{field1,value1},......fieldN,valueN}} 22 | 23 | ![](../images/1289934-20190621232209365-1000366002.png) 24 | 25 | **实战场景:** 26 | 27 | 1.缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。 28 | 29 | ### 3.链表 30 | 31 | List 说白了就是链表(redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。 32 | 33 | ![](../images/1289934-20190621233618769-504231907.png) 34 | 35 | 使用列表的技巧 36 | 37 | - lpush+lpop=Stack(栈) 38 | - lpush+rpop=Queue(队列) 39 | - lpush+ltrim=Capped Collection(有限集合) 40 | - lpush+brpop=Message Queue(消息队列) 41 | 42 | **实战场景:** 43 | 44 | 1.timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。 45 | 46 | ### 4.Set 集合 47 | 48 | 集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。 49 | 50 | 51 | ![](../images/1289934-20190622001013515-677922001.png) 52 | 53 | **实战场景;** 54 | 55 | 1. 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。 56 | 2. 点赞,或点踩,收藏等,可以放到set中实现 57 | 58 | ### 5.zset 有序集合 59 | 60 | 有序集合和集合有着必然的联系,保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,它给每个元素设置一个分数,作为排序的依据。 61 | 62 | (有序集合中的元素不可以重复,但是score 分数 可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同)。 63 | 64 | 65 | ![](../images/1289934-20190622000959260-539243592.png) 66 | 67 | **实战场景:** 68 | 69 | 1. 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/1.458698c8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1.458698c8.jpg -------------------------------------------------------------------------------- /images/1.81971505.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1.81971505.jpg -------------------------------------------------------------------------------- /images/1.f441ca2d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1.f441ca2d.jpg -------------------------------------------------------------------------------- /images/1.fc3a2c47.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1.fc3a2c47.jpg -------------------------------------------------------------------------------- /images/11b30c167de447ffad6d1ab741f921f6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/11b30c167de447ffad6d1ab741f921f6.jpeg -------------------------------------------------------------------------------- /images/1289934-20190621232209365-1000366002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1289934-20190621232209365-1000366002.png -------------------------------------------------------------------------------- /images/1289934-20190621233618769-504231907.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1289934-20190621233618769-504231907.png -------------------------------------------------------------------------------- /images/1289934-20190622000959260-539243592.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1289934-20190622000959260-539243592.png -------------------------------------------------------------------------------- /images/1289934-20190622001013515-677922001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1289934-20190622001013515-677922001.png -------------------------------------------------------------------------------- /images/16530eac181d94c8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16530eac181d94c8.jpg -------------------------------------------------------------------------------- /images/16530eac182b1b66.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16530eac182b1b66.jpg -------------------------------------------------------------------------------- /images/16530eac18882d66.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16530eac18882d66.jpg -------------------------------------------------------------------------------- /images/16c4d9dd1b8235c3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16c4d9dd1b8235c3.jpg -------------------------------------------------------------------------------- /images/16dddb3da13ee747.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dddb3da13ee747.jpg -------------------------------------------------------------------------------- /images/16dddce6e7f823b4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dddce6e7f823b4.jpg -------------------------------------------------------------------------------- /images/16ddddda50800f63.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16ddddda50800f63.jpg -------------------------------------------------------------------------------- /images/16dde266675798b5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dde266675798b5.jpg -------------------------------------------------------------------------------- /images/16dde33e7e6444f3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dde33e7e6444f3.jpg -------------------------------------------------------------------------------- /images/16dde36171697ce0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dde36171697ce0.jpg -------------------------------------------------------------------------------- /images/16dde38e3d6c258f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dde38e3d6c258f.jpg -------------------------------------------------------------------------------- /images/16dde41e63808dc7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/16dde41e63808dc7.jpg -------------------------------------------------------------------------------- /images/17188139512ef9a8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17188139512ef9a8.jpg -------------------------------------------------------------------------------- /images/17188139b47a27ca.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17188139b47a27ca.jpg -------------------------------------------------------------------------------- /images/17188139f32b2558.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17188139f32b2558.jpg -------------------------------------------------------------------------------- /images/1718813a3359a1c0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813a3359a1c0.jpg -------------------------------------------------------------------------------- /images/1718813a71f700a1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813a71f700a1.jpg -------------------------------------------------------------------------------- /images/1718813abb679031.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813abb679031.jpg -------------------------------------------------------------------------------- /images/1718813afa26977e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813afa26977e.jpg -------------------------------------------------------------------------------- /images/1718813b326b7c73.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813b326b7c73.jpg -------------------------------------------------------------------------------- /images/1718813b73c47064.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813b73c47064.jpg -------------------------------------------------------------------------------- /images/1718813bad18d998.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813bad18d998.jpg -------------------------------------------------------------------------------- /images/1718813bec3d6674.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1718813bec3d6674.jpg -------------------------------------------------------------------------------- /images/172346f5e5f0ffab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/172346f5e5f0ffab.jpg -------------------------------------------------------------------------------- /images/17237f45a661cbb7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17237f45a661cbb7.jpg -------------------------------------------------------------------------------- /images/1723d313056fce6a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1723d313056fce6a.jpg -------------------------------------------------------------------------------- /images/1723e7d3872c9406.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1723e7d3872c9406.jpg -------------------------------------------------------------------------------- /images/1723fa894b85ed73.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1723fa894b85ed73.jpg -------------------------------------------------------------------------------- /images/1724037179201fe9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1724037179201fe9.jpg -------------------------------------------------------------------------------- /images/17240a13fa147bea.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17240a13fa147bea.jpg -------------------------------------------------------------------------------- /images/17240afafdc289e5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17240afafdc289e5.jpg -------------------------------------------------------------------------------- /images/1724129062f57007.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1724129062f57007.jpg -------------------------------------------------------------------------------- /images/172412db1d202759.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/172412db1d202759.jpg -------------------------------------------------------------------------------- /images/17241317342078b9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17241317342078b9.jpg -------------------------------------------------------------------------------- /images/17243f19b75d3514.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17243f19b75d3514.jpg -------------------------------------------------------------------------------- /images/1727327958931dbc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1727327958931dbc.jpg -------------------------------------------------------------------------------- /images/172732795e32133c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/172732795e32133c.jpg -------------------------------------------------------------------------------- /images/17273279684debb7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17273279684debb7.jpg -------------------------------------------------------------------------------- /images/1727327968bf0895.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1727327968bf0895.jpg -------------------------------------------------------------------------------- /images/172732797248305f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/172732797248305f.jpg -------------------------------------------------------------------------------- /images/172732798545411e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/172732798545411e.jpg -------------------------------------------------------------------------------- /images/172732798af59f73.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/172732798af59f73.jpg -------------------------------------------------------------------------------- /images/17273279906f511c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17273279906f511c.jpg -------------------------------------------------------------------------------- /images/17273279a6e90503.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17273279a6e90503.jpg -------------------------------------------------------------------------------- /images/17273279bd947317.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/17273279bd947317.jpg -------------------------------------------------------------------------------- /images/1d905286e19f4a3b960d596b184fa8d5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/1d905286e19f4a3b960d596b184fa8d5.jpeg -------------------------------------------------------------------------------- /images/2.fc3a2c47.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/2.fc3a2c47.jpg -------------------------------------------------------------------------------- /images/3.395c1e89.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/3.395c1e89.jpg -------------------------------------------------------------------------------- /images/3.af2b4e20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/3.af2b4e20.jpg -------------------------------------------------------------------------------- /images/4.73e083c6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/4.73e083c6.jpg -------------------------------------------------------------------------------- /images/4.f3a9aa62.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/4.f3a9aa62.jpg -------------------------------------------------------------------------------- /images/5.607471fb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/5.607471fb.jpg -------------------------------------------------------------------------------- /images/5a043effdcc54f7bb67e3fa13fcbb2cc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/5a043effdcc54f7bb67e3fa13fcbb2cc.jpeg -------------------------------------------------------------------------------- /images/6289b6c04279481789a6459dc8da2cb1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/6289b6c04279481789a6459dc8da2cb1.jpeg -------------------------------------------------------------------------------- /images/640.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/640.jpeg -------------------------------------------------------------------------------- /images/640.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/640.webp -------------------------------------------------------------------------------- /images/668722-20180910183925685-224282854.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910183925685-224282854.png -------------------------------------------------------------------------------- /images/668722-20180910183940043-66166526.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910183940043-66166526.png -------------------------------------------------------------------------------- /images/668722-20180910184001095-98860783.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184001095-98860783.png -------------------------------------------------------------------------------- /images/668722-20180910184019522-1324296109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184019522-1324296109.png -------------------------------------------------------------------------------- /images/668722-20180910184047816-693646977.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184047816-693646977.png -------------------------------------------------------------------------------- /images/668722-20180910184108826-1909275147.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184108826-1909275147.png -------------------------------------------------------------------------------- /images/668722-20180910184121575-550443104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184121575-550443104.png -------------------------------------------------------------------------------- /images/668722-20180910184134043-2011419560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184134043-2011419560.png -------------------------------------------------------------------------------- /images/668722-20180910184206679-1690711957.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184206679-1690711957.png -------------------------------------------------------------------------------- /images/668722-20180910184223910-1797391394.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184223910-1797391394.png -------------------------------------------------------------------------------- /images/668722-20180910184241818-998562629.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184241818-998562629.png -------------------------------------------------------------------------------- /images/668722-20180910184255888-841594588.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/668722-20180910184255888-841594588.png -------------------------------------------------------------------------------- /images/745a5d13ff6a4dbc89e287f3188ca111.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/745a5d13ff6a4dbc89e287f3188ca111.jpeg -------------------------------------------------------------------------------- /images/789513781c64485b9130dc239706fbbf.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/789513781c64485b9130dc239706fbbf.jpeg -------------------------------------------------------------------------------- /images/94dee06ef9b64c118916593c0a47f40e.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/94dee06ef9b64c118916593c0a47f40e.jpeg -------------------------------------------------------------------------------- /images/9823874fa8364612b5a4bdf90b705c4c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/9823874fa8364612b5a4bdf90b705c4c.jpeg -------------------------------------------------------------------------------- /images/FiaChoSij8ej.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/FiaChoSij8ej.webp -------------------------------------------------------------------------------- /images/Shu2ohcohb4j.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/Shu2ohcohb4j.jpg -------------------------------------------------------------------------------- /images/Wohph0xohfa9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/Wohph0xohfa9.jpg -------------------------------------------------------------------------------- /images/a2e38eaa9f0541769c7569abffe6da48.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/a2e38eaa9f0541769c7569abffe6da48.jpeg -------------------------------------------------------------------------------- /images/a4d3cb957f3f4b35b280118bb129fe58.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/a4d3cb957f3f4b35b280118bb129fe58.jpeg -------------------------------------------------------------------------------- /images/af2760bc01cc4da6808cd34087b7176a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/af2760bc01cc4da6808cd34087b7176a.jpeg -------------------------------------------------------------------------------- /images/ahwu4aihuo1H.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/ahwu4aihuo1H.jpg -------------------------------------------------------------------------------- /images/array-intersection-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/array-intersection-1.png -------------------------------------------------------------------------------- /images/array-intersection-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/array-intersection-2.png -------------------------------------------------------------------------------- /images/array-intersection-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/array-intersection-3.png -------------------------------------------------------------------------------- /images/array-intersection-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/array-intersection-4.png -------------------------------------------------------------------------------- /images/array-intersection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/array-intersection.png -------------------------------------------------------------------------------- /images/b68dbf0d64844cf8ab83fca534b04e4a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/b68dbf0d64844cf8ab83fca534b04e4a.jpeg -------------------------------------------------------------------------------- /images/b781d9b31ebd4b718de3083cf036fa4b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/b781d9b31ebd4b718de3083cf036fa4b.jpeg -------------------------------------------------------------------------------- /images/bubbleSort.b7d216a5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/bubbleSort.b7d216a5.gif -------------------------------------------------------------------------------- /images/cc285906b45845efa6a5193ec1747ce0.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/cc285906b45845efa6a5193ec1747ce0.jpeg -------------------------------------------------------------------------------- /images/d33af09c79c04875b884f51b0c752eeb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/d33af09c79c04875b884f51b0c752eeb.jpeg -------------------------------------------------------------------------------- /images/dab19075906049dfa6156de45a8bdc1e.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/dab19075906049dfa6156de45a8bdc1e.jpeg -------------------------------------------------------------------------------- /images/deiGoh6JiePh.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/deiGoh6JiePh.webp -------------------------------------------------------------------------------- /images/fd9f917bf17e43d5b7d01ed513e05635.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/fd9f917bf17e43d5b7d01ed513e05635.jpeg -------------------------------------------------------------------------------- /images/giethaD1Eice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/giethaD1Eice.webp -------------------------------------------------------------------------------- /images/ieHie2ur2ooh.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/ieHie2ur2ooh.webp -------------------------------------------------------------------------------- /images/iey3Weim7thu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/iey3Weim7thu.jpg -------------------------------------------------------------------------------- /images/insertionSort.be81c151.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/insertionSort.be81c151.gif -------------------------------------------------------------------------------- /images/longest-common-prefix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/longest-common-prefix.png -------------------------------------------------------------------------------- /images/lru_comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/lru_comparison.png -------------------------------------------------------------------------------- /images/selectionSort.44be35da.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/selectionSort.44be35da.gif -------------------------------------------------------------------------------- /images/zaTh5uayeeT1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei6671/interview-go/0163b8adab07cd4ca49f096c48db75b217889e1d/images/zaTh5uayeeT1.webp -------------------------------------------------------------------------------- /mysql/mysql-index-b-plus.md: -------------------------------------------------------------------------------- 1 | # 为什么MySQL使用B+树做索引? 2 | 3 | 索引这个词,相信大多数人已经相当熟悉了,很多人都知道MySQL的索引主要以B+树为主,但是要问到为什么用B+树,恐怕很少有人能把前因后果讲述的很完整。本文就来从头到尾介绍下数据库的索引。 4 | 5 | 索引是一种数据结构,用于帮助我们在大量数据中快速定位到我们想要查找的数据。 索引最形象的比喻就是图书的目录了。注意这里的大量,数据量大了索引才显得有意义,如果我想要在[1,2,3,4]中找到4这个数据,直接对全数据检索也很快,没有必要费力气建索引再去查找。索引在mysql数据库中分三类: 6 | 7 | B+树索引、Hash索引、全文索引 8 | 9 | 我们今天要介绍的是工作开发中最常接触到innodb存储引擎中的的B+树索引。 10 | 11 | 要介绍B+树索引,就不得不提二叉查找树,平衡二叉树和B树这三种数据结构。B+树就是从他们仨演化来的。 12 | 13 | ## 二叉查找树 14 | 15 | 首先,让我们先看一张图 16 | 17 | ![](../images/640.jpeg) 18 | 19 | 从图中可以看到,我们为user表(用户信息表)建立了一个二叉查找树的索引。图中的圆为二叉查找树的节点,节点中存储了键(key)和数据(data)。 20 | 21 | 键对应user表中的id,数据对应user表中的行数据。二叉查找树的特点就是任何节点的左子节点的键值都小于当前节点的键值,右子节点的键值都大于当前节点的键值。 顶端的节点我们称为根节点,没有子节点的节点我们称之为叶节点。 22 | 23 | 24 | 如果我们需要查找id=12的用户信息,利用我们创建的二叉查找树索引,查找流程如下: 25 | 26 | 1. 将根节点作为当前节点,把12与当前节点的键值10比较,12大于10,接下来我们把当前节点>的右子节点作为当前节点。 27 | 2. 继续把12和当前节点的键值13比较,发现12小于13,把当前节点的左子节点作为当前节点。 28 | 3. 把12和当前节点的键值12对比,12等于12,满足条件,我们从当前节点中取出data,即id=12,name=xm。 29 | 30 | 利用二叉查找树我们只需要3次即可找到匹配的数据。如果在表中一条条的查找的话,我们需要6次才能找到。 31 | 32 | 33 | ## 平衡二叉树 34 | 35 | 上面我们讲解了利用二叉查找树可以快速的找到数据。但是,如果上面的二叉查找树是这样的构造: 36 | 37 | ![](../images/Wohph0xohfa9.jpg) 38 | 39 | 这个时候可以看到我们的二叉查找树变成了一个链表。 40 | 41 | 如果我们需要查找id=17的用户信息,我们需要查找7次,也就相当于全表扫描了。 42 | 43 | 导致这个现象的原因其实是二叉查找树变得不平衡了,也就是高度太高了,从而导致查找效率的不稳定。 44 | 45 | 为了解决这个问题,我们需要保证二叉查找树一直保持平衡,就需要用到平衡二叉树了。 46 | 47 | 48 | 平衡二叉树又称AVL树,在满足二叉查找树特性的基础上,要求每个节点的左右子树的高度差不能超过1。 49 | 50 | 下面是平衡二叉树和非平衡二叉树的对比: 51 | 52 | ![](../images/Shu2ohcohb4j.jpg) 53 | 54 | 55 | 由平衡二叉树的构造我们可以发现第一张图中的二叉树其实就是一棵平衡二叉树。 56 | 57 | 平衡二叉树保证了树的构造是平衡的,当我们插入或删除数据导致不满足平衡二叉树不平衡时,平衡二叉树会进行调整树上的节点来保持平衡。具体的调整方式这里就不介绍了。 58 | 59 | 平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。 60 | 61 | 62 | ## B树 63 | 64 | 因为内存的易失性。一般情况下,我们都会选择将user表中的数据和索引存储在磁盘这种外围设备中。 65 | 66 | 但是和内存相比,从磁盘中读取数据的速度会慢上百倍千倍甚至万倍,所以,我们应当尽量减少从磁盘中读取数据的次数。 另外,从磁盘中读取数据时,都是按照磁盘块来读取的,并不是一条一条的读。 67 | 68 | 如果我们能把尽量多的数据放进磁盘块中,那一次磁盘读取操作就会读取更多数据,那我们查找数据的时间也会大幅度降低。 69 | 70 | 如果我们用树这种数据结构作为索引的数据结构,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说的一个磁盘块,我们都知道平衡二叉树可是每个节点只存储一个键值和数据的。 71 | 72 | 那说明什么? 73 | 74 | 说明每个磁盘块仅仅存储一个键值和数据! 75 | 76 | 那如果我们要存储海量的数据呢? 77 | 78 | 可以想象到二叉树的节点将会非常多,高度也会及其高,我们查找数据时也会进行很多次磁盘IO,我们查找数据的效率将会极低! 79 | 80 | ![](../images/ahwu4aihuo1H.jpg) 81 | 82 | 为了解决平衡二叉树的这个弊端,我们应该寻找一种单个节点可以存储多个键值和数据的平衡树。也就是我们接下来要说的B树。 83 | 84 | B树(Balance Tree)即为平衡树的意思,下图即是一颗B树。 85 | 86 | ![](../images/iey3Weim7thu.jpg) 87 | 88 | 图中的p节点为指向子节点的指针,二叉查找树和平衡二叉树其实也有,因为图的美观性,被省略了。- 图中的每个节点称为页,页就是我们上面说的磁盘块,在mysql中数据读取的基本单位都是页,所以我们这里叫做页更符合mysql中索引的底层数据结构。 89 | 90 | 从上图可以看出,B树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的B树为3阶B树,高度也会很低。 91 | 基于这个特性,B树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。 92 | 93 | 假如我们要查找id=28的用户信息,那么我们在上图B树中查找的流程如下: 94 | 95 | 1. 先找到根节点也就是页1,判断28在键值17和35之间,我们那么我们根据页1中的指针p2找到页3。 96 | 2. 将28和页3中的键值相比较,28在26和30之间,我们根据页3中的指针p2找到页8。 97 | 3. 将28和页8中的键值相比较,发现有匹配的键值28,键值28对应的用户信息为(28,bv)。 98 | 99 | ## B+树 100 | 101 | B+树是对B树的进一步优化。让我们先来看下B+树的结构图: 102 | 103 | ![](../images/deiGoh6JiePh.webp) 104 | 105 | 根据上图我们来看下B+树和B树有什么不同。 106 | 107 | 1. B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。之所以这么做是因为在数据库中页的大小是固定的,innodb中页的默认大小是16KB。如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快。另外,B+树的阶数是等于键值的数量的,如果我们的B+树一个节点可以存储1000个键值,那么3层B+树可以存储1000×1000×1000=10亿个数据。一般根节点是常驻内存的,所以一般我们查找10亿数据,只需要2次磁盘IO。 108 | 2. 因为B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而B树因为数据分散在各个节点,要实现这一点是很不容易的。 109 | 110 | 有心的读者可能还发现上图B+树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。 111 | 其实上面的B树我们也可以对各个节点加上链表。其实这些不是它们之前的区别,是因为在mysql的innodb存储引擎中,索引就是这样存储的。也就是说上图中的B+树索引就是innodb中B+树索引真正的实现方式,准确的说应该是聚集索引(聚集索引和非聚集索引下面会讲到)。 112 | 通过上图可以看到,在innodb中,我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。 113 | MyISAM中的B+树索引实现与innodb中的略有不同。在MyISAM中,B+树索引的叶子节点并不存储数据,而是存储数据的文件地址。 114 | 115 | ## 聚集索引 VS 非聚集索引 116 | 117 | 在上节介绍B+树索引的时候,我们提到了图中的索引其实是聚集索引的实现方式。那什么是聚集索引呢? 118 | 在MySQL中,B+树索引按照存储方式的不同分为聚集索引和非聚集索引。 119 | 这里我们着重介绍innodb中的聚集索引和非聚集索引。 120 | 121 | 1. 聚集索引(聚簇索引):以innodb作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。这是因为innodb是把数据存放在B+树中的,而B+树的键值就是主键,在B+树的叶子节点中,存储了表中所有的数据。这种以主键作为B+树索引的键值而构建的B+树索引,我们称之为聚集索引。 122 | 2. 非聚集索引(非聚簇索引):以主键以外的列值作为键值构建的B+树索引,我们称之为非聚集索引。非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。 123 | 124 | 明白了聚集索引和非聚集索引的定义,我们应该明白这样一句话:数据即索引,索引即数据。 125 | 126 | ### 利用聚集索引和非聚集索引查找数据 127 | 128 | 前面我们讲解B+树索引的时候并没有去说怎么在B+树中进行数据的查找,主要就是因为还没有引出聚集索引和非聚集索引的概念。下面我们通过讲解如何通过聚集索引以及非聚集索引查找数据表中数据的方式介绍一下B+树索引查找数据方法。 129 | 利用聚集索引查找数据 130 | 131 | ![](../images/zaTh5uayeeT1.webp) 132 | 133 | 还是这张B+树索引图,现在我们应该知道这就是聚集索引,表中的数据存储在其中。现在假设我们要查找id>=18并且id<40的用户数据。对应的sql语句为select * from user where id>=18 and id <40,其中id为主键。具体的查找过程如下: 134 | 135 | 1. 一般根节点都是常驻内存的,也就是说页1已经在内存中了,此时不需要到磁盘中读取数据,直接从内存中读取即可。 136 | 从内存中读取到页1,要查找这个id>=18 and id <40或者范围值,我们首先需要找到id=18的键值。 137 | 从页1中我们可以找到键值18,此时我们需要根据指针p2,定位到页3。 138 | 139 | 2. 要从页3中查找数据,我们就需要拿着p2指针去磁盘中进行读取页3。 140 | 从磁盘中读取页3后将页3放入内存中,然后进行查找,我们可以找到键值18,然后再拿到页3中的指针p1,定位到页8。 141 | 142 | 3. 同样的页8页不在内存中,我们需要再去磁盘中将页8读取到内存中。 143 | 将页8读取到内存中后。因为页中的数据是链表进行连接的,而且键值是按照顺序存放的,此时可以根据二分查找法定位到键值18。 144 | 此时因为已经到数据页了,此时我们已经找到一条满足条件的数据了,就是键值18对应的数据。 145 | 因为是范围查找,而且此时所有的数据又都存在叶子节点,并且是有序排列的,那么我们就可以对页8中的键值依次进行遍历查找并匹配满足条件的数据。 146 | 我们可以一直找到键值为22的数据,然后页8中就没有数据了,此时我们需要拿着页8中的p指针去读取页9中的数据。 147 | 148 | 4. 因为页9不在内存中,就又会加载页9到内存中,并通过和页8中一样的方式进行数据的查找,直到将页12加载到内存中,发现41大于40,此时不满足条件。那么查找到此终止。 149 | 最终我们找到满足条件的所有数据为:`(18,kl),(19,kl),(22,hj),(24,io),(25,vg),(29,jk),(31,jk),(33,rt),(34,ty),(35,yu),(37,rt),(39,rt)` 总共12条记录。 150 | 151 | 152 | 下面看下具体的查找流程图: 153 | 154 | ![](../images/ieHie2ur2ooh.webp) 155 | 156 | 利用非聚集索引查找数据 157 | 158 | ![](../images/FiaChoSij8ej.webp) 159 | 160 | 读者看到这张图的时候可能会蒙,这是啥东西啊?怎么都是数字。 161 | 如果有这种感觉,请仔细看下图中红字的解释。什么?还看不懂?那我再来解释下吧。首先,这个非聚集索引表示的是用户幸运数字的索引(为什么是幸运数字?一时兴起想起来的:-)),此时表结构是这样的。 162 | 163 | |id |name| luckyNum| 164 | |----|----|----| 165 | 1| zs| 23 166 | 2| ls| 7 167 | 168 | 在叶子节点中,不在存储所有的数据了,存储的是键值和主键。 169 | 170 | 对于叶子节点中的x-y,比如1-1。左边的1表示的是索引的键值,右边的1表示的是主键值。如果我们要找到幸运数字为33的用户信息,对应的sql语句为select * from user where luckNum=33。 171 | 172 | 查找的流程跟聚集索引一样,这里就不详细介绍了。我们最终会找到主键值47,找到主键后我们需要再到聚集索引中查找具体对应的数据信息,此时又回到了聚集索引的查找流程。 173 | 174 | 下面看下具体的查找流程图: 175 | 176 | ![](../images/giethaD1Eice.webp) 177 | 178 | 在MyISAM中,聚集索引和非聚集索引的叶子节点都会存储数据的文件地址。 179 | 180 | ## 总结 181 | 182 | 本篇文从二叉查找树,详细说明了为什么mysql用B+树作为数据的索引,以及在innodb中数据库如何通过B+树索引来存储数据以及查找数据。我们一定要记住这句话:数据即索引,索引即数据。 183 | 184 | > 原文: [再有人问你为什么MySQL用B+树做索引,就把这篇文章发给她](https://mp.weixin.qq.com/s?__biz=MzIwOTE2MzU4NA==&mid=2247484085&idx=1&sn=92639430ac7ef3e412b550a09bde0115&chksm=9779469aa00ecf8c157e899fe0d5c5b060b282a4e5f2f2f63c187eb3c04d04ef6fad7a1e09e3&token=472896045&lang=zh_CN#rd) 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /mysql/mysql-mvcc.md: -------------------------------------------------------------------------------- 1 | # MySQL InnoDB MVCC 机制的原理及实现 2 | 3 | 4 | ## MVCC 是什么? 5 | 6 | ### 数据库并发控制——锁 7 | 8 | Multiversion (version) concurrency control (MCC or MVCC) 多版本并发控制 ,它是数据库管理系统一种常见的并发控制。 9 | 我们知道并发控制常用的是锁,当线程要对一个共享资源进行操作的时候,加锁是一种非常简单粗暴的方法(事务开始时给 DQL 加读锁,给 DML 加写锁),这种锁是一种 悲观 的实现方式,也就是说这会给其他事务造成堵塞,从而影响数据库性能。 10 | 我来解释一下 乐观锁 和 悲观锁 的概念。我觉得它俩主要是概念的理解。 11 | 12 | - 悲观锁: 当一个线程需要对共享资源进行操作的时候,首先对共享资源进行加锁,当该线程持有该资源的锁的时候,其他线程对该资源进行操作的时候会被 阻塞。比如 Java 中的 Synchronized 关键字。 13 | - 乐观锁:当一个线程需要对一个共享资源进行操作的时候,不对它进行加锁,而是在操作完成之后进行判断。(比如乐观锁会通过一个版本号控制,如果操作完成后通过版本号进行判断在该线程操作过程中是否有其他线程已经对该共享资源进行操作了,如果有则通知操作失败,如果没有则操作成功),当然除了 版本号 还有 CAS,如果不了解的可以去学习一下,这里不做过多涉及。 14 | 15 | ### 数据库并发控制——MVCC 16 | 17 | 很多人认为 MVCC 就是一种 乐观锁 的实现形式,而我认为 MVCC 只是一种 乐观 的实现形式,它是通过 一种 可见性算法 来实现数据库并发控制。 18 | 19 | ### MVCC 的两种读形式 20 | 21 | 在讲 MVCC 的实现原理之前,我觉很有必要先去了解一下 MVCC 的两种读形式。 22 | 23 | - 快照读:读取的只是当前事务的可见版本,不用加锁。而你只要记住 简单的 select 操作就是快照读(select * from table where id = xxx)。 24 | - 当前读:读取的是当前版本,比如 特殊的读操作,更新/插入/删除操作 25 | 26 | 比如: 27 | 28 | ```sql 29 | select * from table where xxx lock in share mode, 30 | select * from table where xxx for update, 31 | update table set.... 32 | insert into table (xxx,xxx) values (xxx,xxx) 33 | delete from table where id = xxx 34 | ``` 35 | 36 | ## MVCC 的实现原理 37 | 38 | MVCC 使用了“三个隐藏字段”来实现版本并发控制,我查了很多资料,看到有很多博客上写的是通过 一个创建事务id字段和一个删除事务id字段 来控制实现的。但后来发现并不是很正确,我们先来看一看 MySQL 在建表的时候 innoDB 创建的真正的三个隐藏列吧。 39 | 40 | |RowID |DB_TRX_ID |DB_ROLL_PTR| id| name |password| 41 | | ---- | ---- | ---- | ---- | ---- | ----| 42 | |自动创建的id |事务id |回滚指针| id| name| password| 43 | 44 | - RowID:隐藏的自增ID,当建表没有指定主键,InnoDB会使用该RowID创建一个聚簇索引。 45 | - DB_TRX_ID:最近修改(更新/删除/插入)该记录的事务ID。 46 | - DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本。 47 | 48 | > 其实还有一个删除的flag字段,用来判断该行记录是否已经被删除。 49 | 50 | 而 MVCC 使用的是其中的 事务字段,回滚指针字段,是否删除字段。我们来看一下现在的表格(isDelete是我自己取的,按照官方说法是在一行开头的content里面,这里其实位置无所谓,你只要知道有就行了)。 51 | 52 | 53 | |isDelete |DB_TRX_ID |DB_ROLL_PTR| id |name |password| 54 | | ---- | ---- | ---- | ---- | ---- |---- | 55 | |true/false| 事务id |回滚指针 |id| name |password| 56 | 57 | 那么如何通过这三个字段来实现 MVCC 的 可见性算法 呢? 58 | 还差点东西! undoLog(回滚日志) 和 read-view(读视图)。 59 | 60 | 61 | - undoLog: 事务的回滚日志,是 可见性算法 的非常重要的部分,分为两类。 62 | - insert undo log:事务在插入新记录产生的undo log,当事务提交之后可以直接丢弃 63 | - update undo log:事务在进行 update 或者 delete 的时候产生的 undo log,在快照读的时候还是需要的,所以不能直接删除,只有当系统没有比这个log更早的read-view了的时候才能删除。ps:所以长事务会产生很多老的视图导致undo log无法删除 大量占用存储空间。 64 | - read-view: 读视图,是MySQL秒级创建视图的必要条件,比如一个事务在进行 select 操作(快照读)的时候会创建一个 read-view ,这个read-view 其实只是三个字段。 65 | - alive_trx_list(我自己取的):read-view生成时刻系统中正在活跃的事务id。 66 | - up_limit_id:记录上面的 alive_trx_list 中的最小事务id。 67 | - low_limit_id:read-view生成时刻,目前已出现的事务ID的最大值 + 1。 68 | 69 | 70 | 71 | 这时候,万事俱备,只欠东风了。下面我来介绍一下,最重要的 可见性算法。 72 | 其实主要思路就是:当生成read-view的时候如何去拿获取的 DB_TRX_ID 去和 read-view 中的三个属性(上面讲了)去作比较。我来说一下三个步骤,如果不是很理解可以参考着我后面的实践结合着去理解。 73 | 74 | - 首先比较这条记录的 DB_TRX_ID 是否是 小于 up_limit_id 或者 等于当前事务id。如果满足,那么说明当前事务能看到这条记录。如果大于则进入下一轮判断 75 | - 然后判断这条记录的 DB_TRX_ID 是否 大于等于 low-limit-id。如果大于等于则说明此事务无法看见该条记录,不然就进入下一轮判断。 76 | - 判断该条记录的 DB_TRX_ID 是否在活跃事务的数组中,如果在则说明这条记录还未提交对于当前操作的事务是不可见的,如果不在则说明已经提交,那么就是可见的。 77 | 78 | > 如果此条记录对于该事务不可见且 ROLL_PTR 不为空那么就会指向回滚指针的地址,通过undolog来查找可见的记录版本。 79 | 80 | 下面我画了一个可见性的算法的流程图 81 | 82 | ![](../images/16dddb3da13ee747.jpg) 83 | 84 | ## 实践 85 | 86 | ### 准备数据 87 | 88 | 首先我创建了一个非常简单的表,只有id和name的学生表。 89 | 90 | |id |name| 91 | |----|----| 92 | |学生id| 学生姓名| 93 | 94 | 这个时候我们将我们需要的隐藏列也标识出来,就变成了这样 95 | 96 | |isDelete |id| |name |DB_TRX_ID |DB_ROLL_PTR| 97 | |----|----|----|----|----|----| 98 | |是否被删除 |学生id |学生姓名| 创建删除更新该记录的事务id |回滚指针| 99 | 100 | 这个时候插入三行数据,将表的数据变成下面这个样子。 101 | 102 | |isDelete |id| name| DB_TRX_ID| DB_ROLL_PTR| 103 | |----|----|----|----|----| 104 | |false |1 |小明 |1 |null 105 | false |2 |小方 |1| null 106 | false |3| 小张 |1| null 107 | 108 | ### 示例一 109 | 110 | ![](../images/16dddce6e7f823b4.jpg) 111 | 112 | 使用过 MySQL 的都知道,因为隔离性,事务 B 此时获取到的数据肯定是这样的。 113 | 114 | |id| name| 115 | |----|----| 116 | 1 |小明 117 | 2 |小方 118 | 3 |小张 119 | 120 | 为什么事务A未提交的修改对于事务B是不可见的,MVCC 是如何做到的?我们用刚刚的可见性算法来实验一下。 121 | 首先事务A开启了事务(当然这不算开启,在RR模式下 真正获取read-view的是在进行第一次进行快照读的时候)。我们假设事务A的事务id为2,事务B的id为3。 122 | 然后事务A进行了更新操作,如图所示,更新操作创建了一个新的版本并且新版本的回滚指针指向了旧的版本(注意 undo log其实存放的是逻辑日志,这里为了方便我直接写成物理日志)。 123 | 124 | ![](../images/16ddddda50800f63.jpg) 125 | 126 | 127 | 最后 事务B 进行了快照读,注意,这是我们分析的重点。 128 | 129 | 首先,在进行快照读的时候我们会创建一个 read-view (忘记回去看一下那三个字段) 这个时候我们的 read-view 是: 130 | ```bash 131 | up-limit-id = 2 132 | alive-trx-list = [2,3] 133 | low-limit-id = 4 134 | ``` 135 | 136 | 然后我们获取那两个没有被修改的记录(没有顺序,这里为了一起解释方便), 我们获取到(2,小方)和(3,小张)这两条记录,发现他们两的 `DB_TRX_ID = 1`, 我们先判断 DB_TRX_ID 是否小于 `up-limit-id` 或者等于当前事务id , 发现 1<2 小于 up-limit-id ,则可见 直接返回视图。 137 | 138 | 然后我们获取更改了的数据行: 139 | 140 | ![](../images/16ddddda50800f63.jpg) 141 | 142 | 其实你也发现了这是一个链表,此时链表头的 DB_TRX_ID 为 2,我们进行判断 2 < 2 不符合,进入下一步判断, 判断 DB_TRX_ID >= low_limit_id 发现此时是 2 >= 4 不符合 故再进入下一步,此时判断 Db_TRX_ID 是否在 alive_trx_list 活跃事务列表中,发现这个 DB_TRX_ID 在活跃列表中,所以只能说明该行记录还未提交,不可见。最终判断不可见之后通过回滚指针查看旧版本,发现此时 DB_TRX_ID 为1,故再次进行判断 DB_TRX_ID < up-limit-id ,此时 1 < 2 符合 ,所以可见并返回, 所以最终返回的是 143 | 144 | |id |name| 145 | |----|----| 146 | 1 |小明 147 | 2 |小方 148 | 3 |小张 149 | 150 | 我们再来验证一下,这个时候我们将事务A提交,重新创建一个事务C并select。 151 | 152 | 我们预期的结果应该是这样的 153 | 154 | |id |name| 155 | |----|----| 156 | 1 |小强 157 | 2 |小方 158 | 3 |小张 159 | 160 | 这个操作的流程图如下 161 | 162 | ![](../images/16dde266675798b5.jpg) 163 | 164 | 这个时候我们再来分析一下 事务c产生的 read-view。 165 | 166 | 这个时候事务A已经提交,所以事务A不在活跃事务数组中,此时 read-view 的三个属性应该是 167 | 168 | ```bash 169 | up-limit-id = 3 170 | alive-trx-list = [3,4] 171 | low-limit-id = 5 172 | ``` 173 | 174 | - 跟上面一样,我们首先获取(2,小方)和(3,小张)这两条记录,发现他们两的 DB_TRX_ID = 1,此时 1 < up-limit-id = 3,故符合可见性,则返回。 175 | - 然后我们获取刚刚被修改的id为1的记录行,发现链表头部的 DB_TRX_ID 为 2, 此时 2 < up-limit-id = 3 故也符合可见性,则返回。 176 | 177 | 所以最终返回的就是 178 | 179 | |id |name| 180 | |----|----| 181 | 1 |小强 182 | 2 |小方 183 | 3 |小张 184 | 185 | ### 示例二 186 | 187 | 为了加深理解,我们再使用一个相对来说比较复杂的示例来验证 可见性算法 。 188 | 189 | ![](../images/16dde33e7e6444f3.jpg) 190 | 191 | 首先我们在事务A中删除一条记录,这个时候就变成了下面的样子。 192 | 193 | ![](../images/16dde36171697ce0.jpg) 194 | 195 | 然后事务B进行了插入,这样就变成了下面这样。 196 | 197 | ![](../images/16dde38e3d6c258f.jpg) 198 | 199 | 然后事务B进行了 select 操作,我们可以发现 这个时候整张表其实会变成这样让这个 select 操作进行选取。 200 | 201 | ![](../images/16dde41e63808dc7.jpg) 202 | 203 | 此时的 read-view 为 204 | 205 | ```bash 206 | up-limit-id = 2 207 | alive-trx-list = [2,3,4] 208 | low-limit-id = 5 209 | ``` 210 | 211 | 这个时候我们进行 快照读,首先对于前面两条小明和小方的记录是一样的,此时 DB_TX_ID 为 1,我们可以判断此时 DB_TX_ID = 1 < up-limit-id = 2 成立故返回。然后判断小张这条记录,首先也是 DB_TX_ID = 2 < up-limit-id = 2 不成立故进入下一轮,DB_TX_ID = 2 >= low-limit-id 不成立再进入最后一轮判断是否在活跃事务列表中,发现 DB_TX_ID = 2 在 alive-trx-list = [2,3,4] 中故不可见(如果可见则会知道前面的删除标志是已经删除,则返回的是空),则根据回滚指针找到上一个版本记录,此时 DB_TX_ID = 1 和上面一样可见则返回该行。 212 | 最后一个判断小亮这条记录,因为 DB_TX_ID = current_tx_id(当前事务id) 所以可见并返回。 213 | 这个时候返回的表则是这样的 214 | 215 | 216 | |id |name| 217 | |----|----| 218 | 1 |小明 219 | 2 |小方 220 | 3 |小张 221 | 4 |小亮 222 | 223 | 然后是事务A进行了select的操作,我们可以得知现在的 read-view 为 224 | 225 | ```bash 226 | up-limit-id = 2 227 | alive-trx-list = [2,3,4] 228 | low-limit-id = 5 229 | ``` 230 | 231 | 然后此时所见和上面也是一样的 232 | 233 | ![](../images/16dde41e63808dc7.jpg) 234 | 235 | 这个时候我们进行 快照读,首先对于前面两条小明和小方的记录是一样的,此时 DB_TX_ID 为 1,我们可以判断此时 DB_TX_ID = 1 < up-limit-id = 2 成立故返回。然后判断小张这条记录,首先 DB_TX_ID = 2 = current_tx_id = 2 成立故返回发现前面的 isDelete 标志为true 则说明已被删除则返回空,对于第四条小亮的也是一样判断 DB_TX_ID = 4 < up-limit-id = 2 不成立进入下一步判断 DB_TX_ID = 4 >= low-limit-id = 5 不成立进入最后一步发现在活跃事务数组中故不可见且此条记录回滚指针为null所以返回空。 236 | 那么此时返回的列表应该就是这样了 237 | 238 | |id| name| 239 | |----|----| 240 | 1 |小明 241 | 2 |小方 242 | 243 | > 虽然要分析很多,但多多益善嘛,多熟悉熟悉就能更深刻理解这个算法了。 244 | 245 | 之后是事务C进行 快照读 操作。首先此时视图还是这个样子 246 | 247 | ![](../images/16dde41e63808dc7.jpg) 248 | 249 | 然后对于事务C的 read-view 为 250 | 251 | ```bash 252 | up-limit-id = 2 253 | alive-trx-list = [2,3,4] 254 | low-limit-id = 5 255 | ``` 256 | 257 | 小明和小方的两条记录和上面一样是可见的这里我就不重复分析了,然后对于小张这条记录 DB_TX_ID = 2 < up-limit-id = 2 || DB_TX_ID == curent_tx_id = 4 不成立故进入下一轮发现 DB_TX_ID >= low-limit-id = 5 更不成立故进入最后一轮发现 DB_TX_ID = 2 在活跃事务数组中故不可见,然后通过回滚指针判断 DB_TX_ID = 1 的小张记录发现可见并返回。最后的小亮也是如此 最后会发现 DB_TX_ID = 3 也在活跃事务数组中故不可见。 258 | 所以事务C select 的结果为 259 | 260 | |id| name| 261 | |----|----| 262 | 1 |小明 263 | 2 |小方 264 | 3 |小张 265 | 266 | 后面事务A和事务B都进行了提交的动作,并且有一个事务D进行了快照读,此时视图还是如此 267 | 268 | ![](../images/16dde41e63808dc7.jpg) 269 | 270 | 但此时的 read-view发生了变化 271 | 272 | ```bash 273 | up-limit-id = 4 274 | alive-trx-list = [4,5] 275 | low-limit-id = 6 276 | ``` 277 | 278 | 我们首先判断小明和小方的记录——可见(不解释了),小张的记录 DB_TX_ID = 2 < up-limit-id = 4 成立故可见,因为前面 isDelete 为 true 则说明删除了返回空,然后小亮的记录 DB_TX_ID = 3 < up-limit-id = 4 成立故可见则返回。所以这次的 select 结果应该是这样的 279 | 280 | |id |name| 281 | |----|----| 282 | 1 |小明 283 | 2 |小方 284 | 4 |小亮 285 | 286 | 最后(真的最后了,不容易吧!),事务C有一次进行了 select 操作。因为在 RR 模式下 read-view 是在第一次快照读的时候确定的,所以此时 read-view是不会更改的,然后前面视图也没有进行更改,所以此时即使前面事务A 事务B已经进行了提交,对于这个时候的事务C的select结果是没有影响的。故结果应该为 287 | 288 | 289 | 290 | |id|name| 291 | |----|----| 292 | 1|小明 293 | 2|小方 294 | 3|小张 295 | 296 | 总结 297 | 我们来总结一下吧。 298 | 299 | 其实 MVCC 是通过 "三个" 隐藏字段 (事务id,回滚指针,删除标志) 加上undo log和可见性算法来实现的版本并发控制。 300 | 301 | 为了你再次深入理解这个算法,我再把这张图挂上来 302 | 303 | ![](../images/16dddb3da13ee747.jpg) 304 | 305 | 306 | >作者:FrancisQ 307 | >链接:https://juejin.im/post/5da8493ae51d4524b25add55 308 | >来源:掘金 309 | >著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 -------------------------------------------------------------------------------- /question/README.md: -------------------------------------------------------------------------------- 1 | ## Golang 常见面试题目解析 2 | 3 | - [交替打印数字和字母](q001.md) 4 | - [判断字符串中字符是否全都不同](q002.md) 5 | - [翻转字符串](q003.md) 6 | - [判断两个给定的字符串排序后是否一致](q004.md) 7 | - [字符串替换问题](q005.md) 8 | - [机器人坐标计算](q006.md) 9 | - [语法题目](q007.md) 10 | - [定时与painc恢复](q012.md) 11 | - [为 sync.WaitGroup 中Wait函数支持 WaitTimeout 功能](q013.md) 12 | - [语法找错题](q014.md) 13 | - [golang 并发题目测试](q015.md) 14 | - [记一道字节跳动的算法面试题](q016.md) 15 | - [多协程查询切片问题](q017.md) 16 | - [多协程查询切片问题](q018.md) 17 | - [多协程查询切片问题](q019.md) 18 | - [字符串转成byte数组,会发生内存拷贝吗?](q020.md) 19 | - [http包的内存泄漏](q021.md) 20 | - [sync.Map 的用法](q022.md) 21 | - [Golang基础语法相关题目](q023.md) -------------------------------------------------------------------------------- /question/q001.md: -------------------------------------------------------------------------------- 1 | ## 交替打印数字和字母 2 | 3 | **问题描述** 4 | 5 | 使用两个 `goroutine` 交替打印序列,一个 `goroutine` 打印数字, 另外一个 `goroutine` 打印字母, 最终效果如下: 6 | 7 | ```bash 8 | 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728 9 | ``` 10 | 11 | **解题思路** 12 | 13 | 问题很简单,使用 channel 来控制打印的进度。使用两个 channel ,来分别控制数字和字母的打印序列, 数字打印完成后通过 channel 通知字母打印, 字母打印完成后通知数字打印,然后周而复始的工作。 14 | 15 | **源码参考** 16 | 17 | ```go 18 | letter,number := make(chan bool),make(chan bool) 19 | wait := sync.WaitGroup{} 20 | 21 | go func() { 22 | i := 1 23 | for { 24 | select { 25 | case <-number: 26 | fmt.Print(i) 27 | i++ 28 | fmt.Print(i) 29 | i++ 30 | letter <- true 31 | } 32 | } 33 | }() 34 | wait.Add(1) 35 | go func(wait *sync.WaitGroup) { 36 | i := 'A' 37 | for{ 38 | select { 39 | case <-letter: 40 | if i >= 'Z' { 41 | wait.Done() 42 | return 43 | } 44 | 45 | fmt.Print(string(i)) 46 | i++ 47 | fmt.Print(string(i)) 48 | i++ 49 | number <- true 50 | } 51 | 52 | } 53 | }(&wait) 54 | number<-true 55 | wait.Wait() 56 | ``` 57 | 58 | **源码解析** 59 | 60 | 这里用到了两个`channel`负责通知,letter负责通知打印字母的goroutine来打印字母,number用来通知打印数字的goroutine打印数字。wait用来等待字母打印完成后退出循环。 61 | 62 | 63 | 也可以分别使用三个 channel 来控制数字,字母以及终止信号的输入. 64 | 65 | ```go 66 | 67 | package main 68 | 69 | import "fmt" 70 | 71 | func main() { 72 | number := make(chan bool) 73 | letter := make(chan bool) 74 | done := make(chan bool) 75 | 76 | go func() { 77 | i := 1 78 | for { 79 | select { 80 | case <-number: 81 | fmt.Print(i) 82 | i++ 83 | fmt.Print(i) 84 | i++ 85 | letter <- true 86 | } 87 | } 88 | }() 89 | 90 | go func() { 91 | j := 'A' 92 | for { 93 | select { 94 | case <-letter: 95 | if j >= 'Z' { 96 | done <- true 97 | } else { 98 | fmt.Print(string(j)) 99 | j++ 100 | fmt.Print(string(j)) 101 | j++ 102 | number <- true 103 | } 104 | } 105 | } 106 | }() 107 | 108 | number <- true 109 | 110 | for { 111 | select { 112 | case <-done: 113 | return 114 | } 115 | } 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /question/q002.md: -------------------------------------------------------------------------------- 1 | ## 判断字符串中字符是否全都不同 2 | 3 | **问题描述** 4 | 5 | 请实现一个算法,确定一个字符串的所有字符【是否全都不同】。这里我们要求【不允许使用额外的存储结构】。 6 | 给定一个string,请返回一个bool值,true代表所有字符全都不同,false代表存在相同的字符。 7 | 保证字符串中的字符为【ASCII字符】。字符串的长度小于等于【3000】。 8 | 9 | 10 | **解题思路** 11 | 12 | 这里有几个重点,第一个是`ASCII字符`,`ASCII字符`字符一共有256个,其中128个是常用字符,可以在键盘上输入。128之后的是键盘上无法找到的。 13 | 14 | 然后是全部不同,也就是字符串中的字符没有重复的,再次,不准使用额外的储存结构,且字符串小于等于3000。 15 | 16 | 如果允许其他额外储存结构,这个题目很好做。如果不允许的话,可以使用golang内置的方式实现。 17 | 18 | **源码参考** 19 | 20 | 通过`strings.Count` 函数判断: 21 | 22 | ``` 23 | func isUniqueString(s string) bool { 24 | if strings.Count(s,"") > 3000{ 25 | return false 26 | } 27 | for _,v := range s { 28 | if v > 127 { 29 | return false 30 | } 31 | if strings.Count(s,string(v)) > 1 { 32 | return false 33 | } 34 | } 35 | return true 36 | } 37 | ``` 38 | 39 | 通过`strings.Index`和`strings.LastIndex`函数判断: 40 | 41 | ``` 42 | func isUniqueString2(s string) bool { 43 | if strings.Count(s,"") > 3000{ 44 | return false 45 | } 46 | for k,v := range s { 47 | if v > 127 { 48 | return false 49 | } 50 | if strings.Index(s,string(v)) != k { 51 | return false 52 | } 53 | } 54 | return true 55 | } 56 | ``` 57 | 58 | 通过位运算判断 59 | 60 | ``` 61 | func isUniqString3(s string) bool { 62 | if len(s) == 0 || len(s) > 3000 { 63 | return false 64 | } 65 | // 256 个字符 256 = 64 + 64 + 64 + 64 66 | var mark1, mark2, mark3, mark4 uint64 67 | var mark *uint64 68 | for _, r := range s { 69 | n := uint64(r) 70 | if n < 64 { 71 | mark = &mark1 72 | } else if n < 128 { 73 | mark = &mark2 74 | n -= 64 75 | } else if n < 192 { 76 | mark = &mark3 77 | n -= 128 78 | } else { 79 | mark = &mark4 80 | n -= 192 81 | } 82 | if (*mark)&(1< 5000 { 20 | return s, false 21 | } 22 | for i := 0; i < l/2; i++ { 23 | str[i], str[l-1-i] = str[l-1-i], str[i] 24 | } 25 | return string(str), true 26 | } 27 | ``` 28 | 29 | **源码解析** 30 | 31 | 以字符串长度的1/2为轴,前后赋值 32 | -------------------------------------------------------------------------------- /question/q004.md: -------------------------------------------------------------------------------- 1 | ## 判断两个给定的字符串排序后是否一致 2 | 3 | **问题描述** 4 | 5 | 给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。 6 | 这里规定【大小写为不同字符】,且考虑字符串重点空格。给定一个string s1和一个string s2,请返回一个bool,代表两串是否重新排列后可相同。 7 | 保证两串的长度都小于等于5000。 8 | 9 | **解题思路** 10 | 11 | 首先要保证字符串长度小于5000。之后只需要一次循环遍历s1中的字符在s2是否都存在即可。 12 | 13 | **源码参考** 14 | 15 | ``` 16 | func isRegroup(s1,s2 string) bool { 17 | sl1 := len([]rune(s1)) 18 | sl2 := len([]rune(s2)) 19 | 20 | if sl1 > 5000 || sl2 > 5000 || sl1 != sl2{ 21 | return false 22 | } 23 | 24 | for _,v := range s1 { 25 | if strings.Count(s1,string(v)) != strings.Count(s2,string(v)) { 26 | return false 27 | } 28 | } 29 | return true 30 | } 31 | ``` 32 | 33 | **源码解析** 34 | 35 | 这里还是使用golang内置方法 `strings.Count` 来判断字符是否一致。 36 | -------------------------------------------------------------------------------- /question/q005.md: -------------------------------------------------------------------------------- 1 | ## 字符串替换问题 2 | 3 | **问题描述** 4 | 5 | 请编写一个方法,将字符串中的空格全部替换为“%20”。 6 | 假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实长度(小于等于1000),同时保证字符串由【大小写的英文字母组成】。 7 | 给定一个string为原始的串,返回替换后的string。 8 | 9 | **解题思路** 10 | 11 | 两个问题,第一个是只能是英文字母,第二个是替换空格。 12 | 13 | **源码参考** 14 | 15 | ``` 16 | func replaceBlank(s string) (string, bool) { 17 | if len([]rune(s)) > 1000 { 18 | return s, false 19 | } 20 | for _, v := range s { 21 | if string(v) != " " && unicode.IsLetter(v) == false { 22 | return s, false 23 | } 24 | } 25 | return strings.Replace(s, " ", "%20", -1), true 26 | } 27 | ``` 28 | 29 | **源码解析** 30 | 31 | 这里使用了golang内置方法`unicode.IsLetter`判断字符是否是字母,之后使用`strings.Replace`来替换空格。 -------------------------------------------------------------------------------- /question/q006.md: -------------------------------------------------------------------------------- 1 | ## 机器人坐标问题 2 | 3 | **问题描述** 4 | 5 | 有一个机器人,给一串指令,L左转 R右转,F前进一步,B后退一步,问最后机器人的坐标,最开始,机器人位于 0 0,方向为正Y。 6 | 可以输入重复指令n : 比如 R2(LF) 这个等于指令 RLFLF。 7 | 问最后机器人的坐标是多少? 8 | 9 | **解题思路** 10 | 11 | 这里的一个难点是解析重复指令。主要指令解析成功,计算坐标就简单了。 12 | 13 | **源码参考** 14 | 15 | ``` 16 | package main 17 | 18 | import ( 19 | "unicode" 20 | ) 21 | 22 | const ( 23 | Left = iota 24 | Top 25 | Right 26 | Bottom 27 | ) 28 | 29 | func main() { 30 | println(move("R2(LF)", 0, 0, Top)) 31 | } 32 | 33 | func move(cmd string, x0 int, y0 int, z0 int) (x, y, z int) { 34 | x, y, z = x0, y0, z0 35 | repeat := 0 36 | repeatCmd := "" 37 | for _, s := range cmd { 38 | switch { 39 | case unicode.IsNumber(s): 40 | repeat = repeat*10 + (int(s) - '0') 41 | case s == ')': 42 | for i := 0; i < repeat; i++ { 43 | x, y, z = move(repeatCmd, x, y, z) 44 | } 45 | repeat = 0 46 | repeatCmd = "" 47 | case repeat > 0 && s != '(' && s != ')': 48 | repeatCmd = repeatCmd + string(s) 49 | case s == 'L': 50 | z = (z + 1) % 4 51 | case s == 'R': 52 | z = (z - 1 + 4) % 4 53 | case s == 'F': 54 | switch { 55 | case z == Left || z == Right: 56 | x = x - z + 1 57 | case z == Top || z == Bottom: 58 | y = y - z + 2 59 | } 60 | case s == 'B': 61 | switch { 62 | case z == Left || z == Right: 63 | x = x + z - 1 64 | case z == Top || z == Bottom: 65 | y = y + z - 2 66 | } 67 | } 68 | } 69 | return 70 | } 71 | 72 | ``` 73 | 74 | **源码解析** 75 | 76 | 这里使用三个值表示机器人当前的状况,分别是:x表示x坐标,y表示y坐标,z表示当前方向。 77 | L、R 命令会改变值z,F、B命令会改变值x、y。 78 | 值x、y的改变还受当前的z值影响。 79 | 80 | 如果是重复指令,那么将重复次数和重复的指令存起来递归调用即可。 81 | -------------------------------------------------------------------------------- /question/q007.md: -------------------------------------------------------------------------------- 1 | ## 常见语法题目 一 2 | 3 | ### 1、下面代码能运行吗?为什么。 4 | 5 | ```go 6 | type Param map[string]interface{} 7 | 8 | type Show struct { 9 | Param 10 | } 11 | 12 | func main1() { 13 | s := new(Show) 14 | s.Param["RMB"] = 10000 15 | } 16 | ``` 17 | 18 | **解析** 19 | 20 | 共发现两个问题: 21 | 22 | 1. `main` 函数不能加数字。 23 | 2. `new` 关键字无法初始化 `Show` 结构体中的 `Param` 属性,所以直接对 `s.Param` 操作会出错。 24 | 25 | ### 2、请说出下面代码存在什么问题。 26 | 27 | ```go 28 | type student struct { 29 | Name string 30 | } 31 | 32 | func zhoujielun(v interface{}) { 33 | switch msg := v.(type) { 34 | case *student, student: 35 | msg.Name 36 | } 37 | } 38 | ``` 39 | 40 | **解析:** 41 | 42 | golang中有规定,`switch type`的`case T1`,类型列表只有一个,那么`v := m.(type)`中的`v`的类型就是T1类型。 43 | 44 | 如果是`case T1, T2`,类型列表中有多个,那`v`的类型还是多对应接口的类型,也就是`m`的类型。 45 | 46 | 所以这里`msg`的类型还是`interface{}`,所以他没有`Name`这个字段,编译阶段就会报错。具体解释见: 47 | 48 | ### 3、写出打印的结果。 49 | 50 | ```go 51 | type People struct { 52 | name string `json:"name"` 53 | } 54 | 55 | func main() { 56 | js := `{ 57 | "name":"11" 58 | }` 59 | var p People 60 | err := json.Unmarshal([]byte(js), &p) 61 | if err != nil { 62 | fmt.Println("err: ", err) 63 | return 64 | } 65 | fmt.Println("people: ", p) 66 | } 67 | ``` 68 | 69 | **解析:** 70 | 71 | 按照 golang 的语法,小写开头的方法、属性或 `struct` 是私有的,同样,在`json` 解码或转码的时候也无法上线私有属性的转换。 72 | 73 | 题目中是无法正常得到`People`的`name`值的。而且,私有属性`name`也不应该加`json`的标签。 74 | 75 | 76 | ### 4、下面的代码是有问题的,请说明原因。 77 | 78 | ```go 79 | type People struct { 80 | Name string 81 | } 82 | 83 | func (p *People) String() string { 84 | return fmt.Sprintf("print: %v", p) 85 | } 86 | 87 | func main() { 88 | p := &People{} 89 | p.String() 90 | } 91 | ``` 92 | 93 | **解析:** 94 | 95 | 在golang中`String() string` 方法实际上是实现了`String`的接口的,该接口定义在`fmt/print.go` 中: 96 | 97 | ```go 98 | type Stringer interface { 99 | String() string 100 | } 101 | ``` 102 | 103 | 在使用 `fmt` 包中的打印方法时,如果类型实现了这个接口,会直接调用。而题目中打印 `p` 的时候会直接调用 `p` 实现的 `String()` 方法,然后就产生了循环调用。 104 | 105 | 106 | ### 5、请找出下面代码的问题所在。 107 | 108 | ```go 109 | func main() { 110 | ch := make(chan int, 1000) 111 | go func() { 112 | for i := 0; i < 10; i++ { 113 | ch <- i 114 | } 115 | }() 116 | go func() { 117 | for { 118 | a, ok := <-ch 119 | if !ok { 120 | fmt.Println("close") 121 | return 122 | } 123 | fmt.Println("a: ", a) 124 | } 125 | }() 126 | close(ch) 127 | fmt.Println("ok") 128 | time.Sleep(time.Second * 100) 129 | } 130 | ``` 131 | 132 | 133 | **解析:** 134 | 135 | 在 golang 中 `goroutine` 的调度时间是不确定的,在题目中,第一个写 `channel` 的 `goroutine` 可能还未调用,或已调用但没有写完时直接 `close` 管道,可能导致写失败,既然出现 `panic` 错误。 136 | 137 | 138 | 139 | ### 6、请说明下面代码书写是否正确。 140 | 141 | ```go 142 | var value int32 143 | 144 | func SetValue(delta int32) { 145 | for { 146 | v := value 147 | if atomic.CompareAndSwapInt32(&value, v, (v+delta)) { 148 | break 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | 155 | 156 | **解析:** 157 | 158 | `atomic.CompareAndSwapInt32` 函数不需要循环调用。 159 | 160 | 161 | ### 7、下面的程序运行后为什么会爆异常。 162 | 163 | ```go 164 | type Project struct{} 165 | 166 | func (p *Project) deferError() { 167 | if err := recover(); err != nil { 168 | fmt.Println("recover: ", err) 169 | } 170 | } 171 | 172 | func (p *Project) exec(msgchan chan interface{}) { 173 | for msg := range msgchan { 174 | m := msg.(int) 175 | fmt.Println("msg: ", m) 176 | } 177 | } 178 | 179 | func (p *Project) run(msgchan chan interface{}) { 180 | for { 181 | defer p.deferError() 182 | go p.exec(msgchan) 183 | time.Sleep(time.Second * 2) 184 | } 185 | } 186 | 187 | func (p *Project) Main() { 188 | a := make(chan interface{}, 100) 189 | go p.run(a) 190 | go func() { 191 | for { 192 | a <- "1" 193 | time.Sleep(time.Second) 194 | } 195 | }() 196 | time.Sleep(time.Second * 100000000000000) 197 | } 198 | 199 | func main() { 200 | p := new(Project) 201 | p.Main() 202 | } 203 | ``` 204 | 205 | **解析:** 206 | 207 | 有一下几个问题: 208 | 209 | 1. `time.Sleep` 的参数数值太大,超过了 `1<<63 - 1` 的限制。 210 | 2. `defer p.deferError()` 需要在协程开始出调用,否则无法捕获 `panic`。 211 | 212 | 213 | ### 8、请说出下面代码哪里写错了 214 | 215 | ```go 216 | func main() { 217 | abc := make(chan int, 1000) 218 | for i := 0; i < 10; i++ { 219 | abc <- i 220 | } 221 | go func() { 222 | for a := range abc { 223 | fmt.Println("a: ", a) 224 | } 225 | }() 226 | close(abc) 227 | fmt.Println("close") 228 | time.Sleep(time.Second * 100) 229 | } 230 | ``` 231 | 232 | **解析:** 233 | 234 | 协程可能还未启动,管道就关闭了。 235 | 236 | 237 | ### 9、请说出下面代码,执行时为什么会报错 238 | 239 | ```go 240 | type Student struct { 241 | name string 242 | } 243 | 244 | func main() { 245 | m := map[string]Student{"people": {"zhoujielun"}} 246 | m["people"].name = "wuyanzu" 247 | } 248 | 249 | ``` 250 | 251 | **解析:** 252 | 253 | map的value本身是不可寻址的,因为map中的值会在内存中移动,并且旧的指针地址在map改变时会变得无效。故如果需要修改map值,可以将`map`中的非指针类型`value`,修改为指针类型,比如使用`map[string]*Student`. 254 | 255 | 256 | ### 10、请说出下面的代码存在什么问题? 257 | 258 | ```go 259 | type query func(string) string 260 | 261 | func exec(name string, vs ...query) string { 262 | ch := make(chan string) 263 | fn := func(i int) { 264 | ch <- vs[i](name) 265 | } 266 | for i, _ := range vs { 267 | go fn(i) 268 | } 269 | return <-ch 270 | } 271 | 272 | func main() { 273 | ret := exec("111", func(n string) string { 274 | return n + "func1" 275 | }, func(n string) string { 276 | return n + "func2" 277 | }, func(n string) string { 278 | return n + "func3" 279 | }, func(n string) string { 280 | return n + "func4" 281 | }) 282 | fmt.Println(ret) 283 | } 284 | ``` 285 | 286 | **解析:** 287 | 288 | 依据4个goroutine的启动后执行效率,很可能打印111func4,但其他的111func*也可能先执行,exec只会返回一条信息。 289 | 290 | 291 | ### 11、下面这段代码为什么会卡死? 292 | 293 | ```go 294 | package main 295 | 296 | import ( 297 | "fmt" 298 | "runtime" 299 | ) 300 | 301 | func main() { 302 | var i byte 303 | go func() { 304 | for i = 0; i <= 255; i++ { 305 | } 306 | }() 307 | fmt.Println("Dropping mic") 308 | // Yield execution to force executing other goroutines 309 | runtime.Gosched() 310 | runtime.GC() 311 | fmt.Println("Done") 312 | } 313 | ``` 314 | 315 | **解析:** 316 | 317 | Golang 中,byte 其实被 alias 到 uint8 上了。所以上面的 for 循环会始终成立,因为 i++ 到 i=255 的时候会溢出,i <= 255 一定成立。 318 | 319 | 也即是, for 循环永远无法退出,所以上面的代码其实可以等价于这样: 320 | ```go 321 | go func() { 322 | for {} 323 | } 324 | ``` 325 | 326 | 正在被执行的 goroutine 发生以下情况时让出当前 goroutine 的执行权,并调度后面的 goroutine 执行: 327 | 328 | - IO 操作 329 | - Channel 阻塞 330 | - system call 331 | - 运行较长时间 332 | 333 | 如果一个 goroutine 执行时间太长,scheduler 会在其 G 对象上打上一个标志( preempt),当这个 goroutine 内部发生函数调用的时候,会先主动检查这个标志,如果为 true 则会让出执行权。 334 | 335 | main 函数里启动的 goroutine 其实是一个没有 IO 阻塞、没有 Channel 阻塞、没有 system call、没有函数调用的死循环。 336 | 337 | 也就是,它无法主动让出自己的执行权,即使已经执行很长时间,scheduler 已经标志了 preempt。 338 | 339 | 而 golang 的 GC 动作是需要所有正在运行 `goroutine` 都停止后进行的。因此,程序会卡在 `runtime.GC()` 等待所有协程退出。 340 | -------------------------------------------------------------------------------- /question/q008.md: -------------------------------------------------------------------------------- 1 | ## 常见语法题目 二 2 | 3 | 4 | ### 1、写出下面代码输出内容。 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | func main() { 14 | defer_call() 15 | } 16 | 17 | func defer_call() { 18 | defer func() { fmt.Println("打印前") }() 19 | defer func() { fmt.Println("打印中") }() 20 | defer func() { fmt.Println("打印后") }() 21 | 22 | panic("触发异常") 23 | } 24 | ``` 25 | 26 | **解析:** 27 | 28 | `defer` 关键字的实现跟go关键字很类似,不同的是它调用的是`runtime.deferproc`而不是`runtime.newproc`。 29 | 30 | 在`defer`出现的地方,插入了指令`call runtime.deferproc`,然后在函数返回之前的地方,插入指令`call runtime.deferreturn`。 31 | 32 | goroutine的控制结构中,有一张表记录`defer`,调用`runtime.deferproc`时会将需要defer的表达式记录在表中,而在调用`runtime.deferreturn`的时候,则会依次从defer表中出栈并执行。 33 | 34 | 因此,题目最后输出顺序应该是`defer` 定义顺序的倒序。`panic` 错误并不能终止 `defer` 的执行。 35 | 36 | ### 2、 以下代码有什么问题,说明原因 37 | 38 | ```go 39 | type student struct { 40 | Name string 41 | Age int 42 | } 43 | 44 | func pase_student() { 45 | m := make(map[string]*student) 46 | stus := []student{ 47 | {Name: "zhou", Age: 24}, 48 | {Name: "li", Age: 23}, 49 | {Name: "wang", Age: 22}, 50 | } 51 | for _, stu := range stus { 52 | m[stu.Name] = &stu 53 | } 54 | } 55 | ``` 56 | 57 | **解析:** 58 | 59 | golang 的 `for ... range` 语法中,`stu` 变量会被复用,每次循环会将集合中的值复制给这个变量,因此,会导致最后`m`中的`map`中储存的都是`stus`最后一个`student`的值。 60 | 61 | 62 | ### 3、下面的代码会输出什么,并说明原因 63 | 64 | ```go 65 | func main() { 66 | runtime.GOMAXPROCS(1) 67 | wg := sync.WaitGroup{} 68 | wg.Add(20) 69 | for i := 0; i < 10; i++ { 70 | go func() { 71 | fmt.Println("i: ", i) 72 | wg.Done() 73 | }() 74 | } 75 | for i := 0; i < 10; i++ { 76 | go func(i int) { 77 | fmt.Println("i: ", i) 78 | wg.Done() 79 | }(i) 80 | } 81 | wg.Wait() 82 | } 83 | 84 | ``` 85 | 86 | **解析:** 87 | 88 | 这个输出结果决定来自于调度器优先调度哪个G。从runtime的源码可以看到,当创建一个G时,会优先放入到下一个调度的`runnext`字段上作为下一次优先调度的G。因此,最先输出的是最后创建的G,也就是9. 89 | 90 | ```go 91 | func newproc(siz int32, fn *funcval) { 92 | argp := add(unsafe.Pointer(&fn), sys.PtrSize) 93 | gp := getg() 94 | pc := getcallerpc() 95 | systemstack(func() { 96 | newg := newproc1(fn, argp, siz, gp, pc) 97 | 98 | _p_ := getg().m.p.ptr() 99 | //新创建的G会调用这个方法来决定如何调度 100 | runqput(_p_, newg, true) 101 | 102 | if mainStarted { 103 | wakep() 104 | } 105 | }) 106 | } 107 | ... 108 | 109 | if next { 110 | retryNext: 111 | oldnext := _p_.runnext 112 | //当next是true时总会将新进来的G放入下一次调度字段中 113 | if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) { 114 | goto retryNext 115 | } 116 | if oldnext == 0 { 117 | return 118 | } 119 | // Kick the old runnext out to the regular run queue. 120 | gp = oldnext.ptr() 121 | } 122 | ``` 123 | 124 | 125 | ### 4、下面代码会输出什么? 126 | 127 | ```go 128 | type People struct{} 129 | 130 | func (p *People) ShowA() { 131 | fmt.Println("showA") 132 | p.ShowB() 133 | } 134 | func (p *People) ShowB() { 135 | fmt.Println("showB") 136 | } 137 | 138 | type Teacher struct { 139 | People 140 | } 141 | 142 | func (t *Teacher) ShowB() { 143 | fmt.Println("teacher showB") 144 | } 145 | 146 | func main() { 147 | t := Teacher{} 148 | t.ShowA() 149 | } 150 | 151 | 152 | ``` 153 | 154 | 155 | **解析:** 156 | 157 | 输出结果为`showA`、`showB`。golang 语言中没有继承概念,只有组合,也没有虚方法,更没有重载。因此,`*Teacher` 的 `ShowB` 不会覆写被组合的 `People` 的方法。 158 | 159 | 160 | ### 5、下面代码会触发异常吗?请详细说明 161 | 162 | ```go 163 | func main() { 164 | runtime.GOMAXPROCS(1) 165 | int_chan := make(chan int, 1) 166 | string_chan := make(chan string, 1) 167 | int_chan <- 1 168 | string_chan <- "hello" 169 | select { 170 | case value := <-int_chan: 171 | fmt.Println(value) 172 | case value := <-string_chan: 173 | panic(value) 174 | } 175 | } 176 | ``` 177 | 178 | **解析:** 179 | 180 | 结果是随机执行。golang 在多个`case` 可读的时候会公平的选中一个执行。 181 | 182 | ### 6、下面代码输出什么? 183 | 184 | ```go 185 | func calc(index string, a, b int) int { 186 | ret := a + b 187 | fmt.Println(index, a, b, ret) 188 | return ret 189 | } 190 | 191 | func main() { 192 | a := 1 193 | b := 2 194 | defer calc("1", a, calc("10", a, b)) 195 | a = 0 196 | defer calc("2", a, calc("20", a, b)) 197 | b = 1 198 | } 199 | ``` 200 | 201 | **解析:** 202 | 203 | 输出结果为: 204 | 205 | ``` 206 | 10 1 2 3 207 | 20 0 2 2 208 | 2 0 2 2 209 | 1 1 3 4 210 | ``` 211 | 212 | `defer` 在定义的时候会计算好调用函数的参数,所以会优先输出`10`、`20` 两个参数。然后根据定义的顺序倒序执行。 213 | 214 | 215 | ### 7、请写出以下输入内容 216 | 217 | ```go 218 | func main() { 219 | s := make([]int, 5) 220 | s = append(s, 1, 2, 3) 221 | fmt.Println(s) 222 | } 223 | 224 | ``` 225 | 226 | **解析:** 227 | 228 | 输出为 `0 0 0 0 0 1 2 3`。 229 | 230 | `make` 在初始化切片时指定了长度,所以追加数据时会从`len(s)` 位置开始填充数据。 231 | 232 | 233 | ### 8、下面的代码有什么问题? 234 | 235 | ```go 236 | type UserAges struct { 237 | ages map[string]int 238 | sync.Mutex 239 | } 240 | 241 | func (ua *UserAges) Add(name string, age int) { 242 | ua.Lock() 243 | defer ua.Unlock() 244 | ua.ages[name] = age 245 | } 246 | 247 | func (ua *UserAges) Get(name string) int { 248 | if age, ok := ua.ages[name]; ok { 249 | return age 250 | } 251 | return -1 252 | } 253 | ``` 254 | 255 | **解析:** 256 | 257 | 在执行 Get方法时可能被thorw。 258 | 259 | 虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变量,此时同时读写资源存在竞争关系。会报错误信息:“fatal error: concurrent map read and map write”。 260 | 261 | 因此,在 `Get` 中也需要加锁,因为这里只是读,建议使用读写锁 `sync.RWMutex`。 262 | 263 | 264 | ### 9、下面的迭代会有什么问题? 265 | 266 | ```go 267 | func (set *threadSafeSet) Iter() <-chan interface{} { 268 | ch := make(chan interface{}) 269 | go func() { 270 | set.RLock() 271 | 272 | for elem := range set.s { 273 | ch <- elem 274 | } 275 | 276 | close(ch) 277 | set.RUnlock() 278 | 279 | }() 280 | return ch 281 | } 282 | ``` 283 | 284 | **解析:** 285 | 286 | 默认情况下 `make` 初始化的 `channel` 是无缓冲的,也就是在迭代写时会阻塞。 287 | 288 | ### 10、以下代码能编译过去吗?为什么? 289 | 290 | ```go 291 | package main 292 | 293 | import ( 294 | "fmt" 295 | ) 296 | 297 | type People interface { 298 | Speak(string) string 299 | } 300 | 301 | type Student struct{} 302 | 303 | func (stu *Student) Speak(think string) (talk string) { 304 | if think == "bitch" { 305 | talk = "You are a good boy" 306 | } else { 307 | talk = "hi" 308 | } 309 | return 310 | } 311 | 312 | func main() { 313 | var peo People = Student{} 314 | think := "bitch" 315 | fmt.Println(peo.Speak(think)) 316 | } 317 | ``` 318 | 319 | **解析:** 320 | 321 | 322 | 编译失败,值类型 `Student{}` 未实现接口`People`的方法,不能定义为 `People`类型。 323 | 324 | 在 golang 语言中,`Student` 和 `*Student` 是两种类型,第一个是表示 `Student` 本身,第二个是指向 `Student` 的指针。 325 | 326 | 327 | 328 | 329 | ### 11、以下代码打印出来什么内容,说出为什么。。。 330 | 331 | ```go 332 | package main 333 | 334 | import ( 335 | "fmt" 336 | ) 337 | 338 | type People interface { 339 | Show() 340 | } 341 | 342 | type Student struct{} 343 | 344 | func (stu *Student) Show() { 345 | 346 | } 347 | 348 | func live() People { 349 | var stu *Student 350 | return stu 351 | } 352 | 353 | func main() { 354 | if live() == nil { 355 | fmt.Println("AAAAAAA") 356 | } else { 357 | fmt.Println("BBBBBBB") 358 | } 359 | } 360 | ``` 361 | 362 | **解析:** 363 | 364 | 跟上一题一样,不同的是`*Student` 的定义后本身没有初始化值,所以 `*Student` 是 `nil`的,但是`*Student` 实现了 `People` 接口,接口不为 `nil`。 365 | -------------------------------------------------------------------------------- /question/q009.md: -------------------------------------------------------------------------------- 1 | # 在 golang 协程和channel配合使用 2 | 3 | > 写代码实现两个 goroutine,其中一个产生随机数并写入到 go channel 中,另外一个从 channel 中读取数字并打印到标准输出。最终输出五个随机数。 4 | 5 | **解析** 6 | 7 | 这是一道很简单的golang基础题目,实现方法也有很多种,一般想答让面试官满意的答案还是有几点注意的地方。 8 | 9 | 1. `goroutine` 在golang中式非阻塞的 10 | 2. `channel` 无缓冲情况下,读写都是阻塞的,且可以用`for`循环来读取数据,当管道关闭后,`for` 退出。 11 | 3. golang 中有专用的`select case` 语法从管道读取数据。 12 | 13 | 示例代码如下: 14 | 15 | ```go 16 | func main() { 17 | out := make(chan int) 18 | wg := sync.WaitGroup{} 19 | wg.Add(2) 20 | go func() { 21 | defer wg.Done() 22 | for i := 0; i < 5; i++ { 23 | out <- rand.Intn(5) 24 | } 25 | close(out) 26 | }() 27 | go func() { 28 | defer wg.Done() 29 | for i := range out { 30 | fmt.Println(i) 31 | } 32 | }() 33 | wg.Wait() 34 | } 35 | ``` 36 | 37 | 如果不想使用 `sync.WaitGroup`, 也可以用一个 `done` channel. 38 | ```go 39 | package main 40 | 41 | import ( 42 | "fmt" 43 | "math/rand" 44 | ) 45 | 46 | func main() { 47 | random := make(chan int) 48 | done := make(chan bool) 49 | 50 | go func() { 51 | for { 52 | num, ok := <-random 53 | if ok { 54 | fmt.Println(num) 55 | } else { 56 | done <- true 57 | } 58 | } 59 | }() 60 | 61 | go func() { 62 | defer close(random) 63 | 64 | for i := 0; i < 5; i++ { 65 | random <- rand.Intn(5) 66 | } 67 | }() 68 | 69 | <-done 70 | close(done) 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /question/q010.md: -------------------------------------------------------------------------------- 1 | # 实现阻塞读且并发安全的map 2 | 3 | GO里面MAP如何实现key不存在 get操作等待 直到key存在或者超时,保证并发安全,且需要实现以下接口: 4 | 5 | ```go 6 | type sp interface { 7 | Out(key string, val interface{}) //存入key /val,如果该key读取的goroutine挂起,则唤醒。此方法不会阻塞,时刻都可以立即执行并返回 8 | Rd(key string, timeout time.Duration) interface{} //读取一个key,如果key不存在阻塞,等待key存在或者超时 9 | } 10 | ``` 11 | 12 | **解析:** 13 | 14 | 看到阻塞协程第一个想到的就是`channel`,题目中要求并发安全,那么必须用锁,还要实现多个`goroutine`读的时候如果值不存在则阻塞,直到写入值,那么每个键值需要有一个阻塞`goroutine` 的 `channel`。 15 | 16 | [实现如下:](../src/q010.go) 17 | 18 | ```go 19 | type Map struct { 20 | c map[string]*entry 21 | rmx *sync.RWMutex 22 | } 23 | type entry struct { 24 | ch chan struct{} 25 | value interface{} 26 | isExist bool 27 | } 28 | 29 | func (m *Map) Out(key string, val interface{}) { 30 | m.rmx.Lock() 31 | defer m.rmx.Unlock() 32 | item, ok := m.c[key] 33 | if !ok { 34 | m.c[key] = &entry{ 35 | value: val, 36 | isExist: true, 37 | } 38 | return 39 | } 40 | item.value = val 41 | if !item.isExist { 42 | if item.ch != nil { 43 | close(item.ch) 44 | item.ch = nil 45 | } 46 | } 47 | return 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /question/q011.md: -------------------------------------------------------------------------------- 1 | # 高并发下的锁与map的读写 2 | 3 | 场景:在一个高并发的web服务器中,要限制IP的频繁访问。现模拟100个IP同时并发访问服务器,每个IP要重复访问1000次。 4 | 5 | 每个IP三分钟之内只能访问一次。修改以下代码完成该过程,要求能成功输出 success:100 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "time" 13 | ) 14 | 15 | type Ban struct { 16 | visitIPs map[string]time.Time 17 | } 18 | 19 | func NewBan() *Ban { 20 | return &Ban{visitIPs: make(map[string]time.Time)} 21 | } 22 | func (o *Ban) visit(ip string) bool { 23 | if _, ok := o.visitIPs[ip]; ok { 24 | return true 25 | } 26 | o.visitIPs[ip] = time.Now() 27 | return false 28 | } 29 | func main() { 30 | success := 0 31 | ban := NewBan() 32 | for i := 0; i < 1000; i++ { 33 | for j := 0; j < 100; j++ { 34 | go func() { 35 | ip := fmt.Sprintf("192.168.1.%d", j) 36 | if !ban.visit(ip) { 37 | success++ 38 | } 39 | }() 40 | } 41 | 42 | } 43 | fmt.Println("success:", success) 44 | } 45 | ``` 46 | 47 | **解析** 48 | 49 | 该问题主要考察了并发情况下map的读写问题,而给出的初始代码,又存在`for`循环中启动`goroutine`时变量使用问题以及`goroutine`执行滞后问题。 50 | 51 | 因此,首先要保证启动的`goroutine`得到的参数是正确的,然后保证`map`的并发读写,最后保证三分钟只能访问一次。 52 | 53 | 多CPU核心下修改`int`的值极端情况下会存在不同步情况,因此需要原子性的修改int值。 54 | 55 | 下面给出的实例代码,是启动了一个协程每分钟检查一下`map`中的过期`ip`,`for`启动协程时传参。 56 | 57 | ```go 58 | package main 59 | 60 | import ( 61 | "context" 62 | "fmt" 63 | "sync" 64 | "sync/atomic" 65 | "time" 66 | ) 67 | 68 | type Ban struct { 69 | visitIPs map[string]time.Time 70 | lock sync.Mutex 71 | } 72 | 73 | func NewBan(ctx context.Context) *Ban { 74 | o := &Ban{visitIPs: make(map[string]time.Time)} 75 | go func() { 76 | timer := time.NewTimer(time.Minute * 1) 77 | for { 78 | select { 79 | case <-timer.C: 80 | o.lock.Lock() 81 | for k, v := range o.visitIPs { 82 | if time.Now().Sub(v) >= time.Minute*1 { 83 | delete(o.visitIPs, k) 84 | } 85 | } 86 | o.lock.Unlock() 87 | timer.Reset(time.Minute * 1) 88 | case <-ctx.Done(): 89 | return 90 | } 91 | } 92 | }() 93 | return o 94 | } 95 | func (o *Ban) visit(ip string) bool { 96 | o.lock.Lock() 97 | defer o.lock.Unlock() 98 | if _, ok := o.visitIPs[ip]; ok { 99 | return true 100 | } 101 | o.visitIPs[ip] = time.Now() 102 | return false 103 | } 104 | func main() { 105 | success := int64(0) 106 | ctx, cancel := context.WithCancel(context.Background()) 107 | defer cancel() 108 | 109 | ban := NewBan(ctx) 110 | 111 | wait := &sync.WaitGroup{} 112 | 113 | wait.Add(1000 * 100) 114 | for i := 0; i < 1000; i++ { 115 | for j := 0; j < 100; j++ { 116 | go func(j int) { 117 | defer wait.Done() 118 | ip := fmt.Sprintf("192.168.1.%d", j) 119 | if !ban.visit(ip) { 120 | atomic.AddInt64(&success, 1) 121 | } 122 | }(j) 123 | } 124 | 125 | } 126 | wait.Wait() 127 | 128 | fmt.Println("success:", success) 129 | } 130 | ``` -------------------------------------------------------------------------------- /question/q012.md: -------------------------------------------------------------------------------- 1 | ## 1. 写出以下逻辑,要求每秒钟调用一次proc并保证程序不退出? 2 | 3 | ```go 4 | package main 5 | 6 | func main() { 7 | go func() { 8 | // 1 在这里需要你写算法 9 | // 2 要求每秒钟调用一次proc函数 10 | // 3 要求程序不能退出 11 | }() 12 | 13 | select {} 14 | } 15 | 16 | func proc() { 17 | panic("ok") 18 | } 19 | ``` 20 | 21 | **解析** 22 | 23 | 题目主要考察了两个知识点: 24 | 25 | 1. 定时执行执行任务 26 | 2. 捕获 panic 错误 27 | 28 | 题目中要求每秒钟执行一次,首先想到的就是 `time.Ticker`对象,该函数可每秒钟往`chan`中放一个`Time`,正好符合我们的要求。 29 | 30 | 在 `golang` 中捕获 `panic` 一般会用到 `recover()` 函数。 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "fmt" 37 | "time" 38 | ) 39 | 40 | func main() { 41 | go func() { 42 | // 1 在这里需要你写算法 43 | // 2 要求每秒钟调用一次proc函数 44 | // 3 要求程序不能退出 45 | 46 | t := time.NewTicker(time.Second * 1) 47 | for { 48 | select { 49 | case <-t.C: 50 | go func() { 51 | defer func() { 52 | if err := recover(); err != nil { 53 | fmt.Println(err) 54 | } 55 | }() 56 | proc() 57 | }() 58 | } 59 | } 60 | }() 61 | 62 | select {} 63 | } 64 | 65 | func proc() { 66 | panic("ok") 67 | } 68 | 69 | ``` -------------------------------------------------------------------------------- /question/q013.md: -------------------------------------------------------------------------------- 1 | ## 为 sync.WaitGroup 中Wait函数支持 WaitTimeout 功能.` 2 | ` 3 | ```go 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | wg := sync.WaitGroup{} 14 | c := make(chan struct{}) 15 | for i := 0; i < 10; i++ { 16 | wg.Add(1) 17 | go func(num int, close <-chan struct{}) { 18 | defer wg.Done() 19 | <-close 20 | fmt.Println(num) 21 | }(i, c) 22 | } 23 | 24 | if WaitTimeout(&wg, time.Second*5) { 25 | close(c) 26 | fmt.Println("timeout exit") 27 | } 28 | time.Sleep(time.Second * 10) 29 | } 30 | 31 | func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { 32 | // 要求手写代码 33 | // 要求sync.WaitGroup支持timeout功能 34 | // 如果timeout到了超时时间返回true 35 | // 如果WaitGroup自然结束返回false 36 | } 37 | ``` 38 | 39 | 40 | **解析** 41 | 42 | 首先 `sync.WaitGroup` 对象的 `Wait` 函数本身是阻塞的,同时,超时用到的`time.Timer` 对象也需要阻塞的读。 43 | 44 | 同时阻塞的两个对象肯定要每个启动一个协程,每个协程去处理一个阻塞,难点在于怎么知道哪个阻塞先完成。 45 | 46 | 目前我用的方式是声明一个没有缓冲的`chan`,谁先完成谁优先向管道中写入数据。 47 | 48 | ```go 49 | package main 50 | 51 | import ( 52 | "fmt" 53 | "sync" 54 | "time" 55 | ) 56 | 57 | func main() { 58 | wg := sync.WaitGroup{} 59 | c := make(chan struct{}) 60 | for i := 0; i < 10; i++ { 61 | wg.Add(1) 62 | go func(num int, close <-chan struct{}) { 63 | defer wg.Done() 64 | <-close 65 | fmt.Println(num) 66 | }(i, c) 67 | } 68 | 69 | if WaitTimeout(&wg, time.Second*5) { 70 | close(c) 71 | fmt.Println("timeout exit") 72 | } 73 | time.Sleep(time.Second * 10) 74 | } 75 | 76 | func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { 77 | // 要求手写代码 78 | // 要求sync.WaitGroup支持timeout功能 79 | // 如果timeout到了超时时间返回true 80 | // 如果WaitGroup自然结束返回false 81 | ch := make(chan bool, 1) 82 | 83 | go time.AfterFunc(timeout, func() { 84 | ch <- true 85 | }) 86 | 87 | go func() { 88 | wg.Wait() 89 | ch <- false 90 | }() 91 | 92 | return <- ch 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /question/q014.md: -------------------------------------------------------------------------------- 1 | # 语法找错题 2 | 3 | ## 写出以下代码出现的问题 4 | 5 | ```go 6 | package main 7 | import ( 8 | "fmt" 9 | ) 10 | func main() { 11 | var x string = nil 12 | if x == nil { 13 | x = "default" 14 | } 15 | fmt.Println(x) 16 | } 17 | ``` 18 | 19 | golang 中字符串是不能赋值 `nil` 的,也不能跟 `nil` 比较。 20 | 21 | ## 写出以下打印内容 22 | 23 | ```go 24 | package main 25 | import "fmt" 26 | const ( 27 | a = iota 28 | b = iota 29 | ) 30 | const ( 31 | name = "menglu" 32 | c = iota 33 | d = iota 34 | ) 35 | func main() { 36 | fmt.Println(a) 37 | fmt.Println(b) 38 | fmt.Println(c) 39 | fmt.Println(d) 40 | } 41 | ``` 42 | 43 | ## 找出下面代码的问题 44 | 45 | ```go 46 | package main 47 | import "fmt" 48 | type query func(string) string 49 | 50 | func exec(name string, vs ...query) string { 51 | ch := make(chan string) 52 | fn := func(i int) { 53 | ch <- vs[i](name) 54 | } 55 | for i, _ := range vs { 56 | go fn(i) 57 | } 58 | return <-ch 59 | } 60 | 61 | func main() { 62 | ret := exec("111", func(n string) string { 63 | return n + "func1" 64 | }, func(n string) string { 65 | return n + "func2" 66 | }, func(n string) string { 67 | return n + "func3" 68 | }, func(n string) string { 69 | return n + "func4" 70 | }) 71 | fmt.Println(ret) 72 | } 73 | ``` 74 | 75 | 上面的代码有严重的内存泄漏问题,出错的位置是 `go fn(i)`,实际上代码执行后会启动 4 个协程,但是因为 `ch` 是非缓冲的,只可能有一个协程写入成功。而其他三个协程会一直在后台等待写入。 76 | 77 | ## 写出以下打印结果,并解释下为什么这么打印的。 78 | 79 | ```go 80 | package main 81 | import ( 82 | "fmt" 83 | ) 84 | func main() { 85 | str1 := []string{"a", "b", "c"} 86 | str2 := str1[1:] 87 | str2[1] = "new" 88 | fmt.Println(str1) 89 | str2 = append(str2, "z", "x", "y") 90 | fmt.Println(str1) 91 | } 92 | ``` 93 | 94 | golang 中的切片底层其实使用的是数组。当使用`str1[1:]` 使,`str2` 和 `str1` 底层共享一个数组,这回导致 `str2[1] = "new"` 语句影响 `str1`。 95 | 96 | 而 `append` 会导致底层数组扩容,生成新的数组,因此追加数据后的 `str2` 不会影响 `str1`。 97 | 98 | 但是为什么对 `str2` 复制后影响的确实 `str1` 的第三个元素呢?这是因为切片 `str2` 是从数组的第二个元素开始,`str2` 索引为 1 的元素对应的是 `str1` 索引为 2 的元素。 99 | 100 | ## 写出以下打印结果 101 | 102 | ```go 103 | package main 104 | 105 | import ( 106 | "fmt" 107 | ) 108 | 109 | type Student struct { 110 | Name string 111 | } 112 | 113 | func main() { 114 | fmt.Println(&Student{Name: "menglu"} == &Student{Name: "menglu"}) 115 | fmt.Println(Student{Name: "menglu"} == Student{Name: "menglu"}) 116 | } 117 | ``` 118 | 119 | 个人理解:指针类型比较的是指针地址,非指针类型比较的是每个属性的值。 120 | 121 | ## 写出以下代码的问题 122 | 123 | ```go 124 | package main 125 | 126 | import ( 127 | "fmt" 128 | ) 129 | 130 | func main() { 131 | fmt.Println([...]string{"1"} == [...]string{"1"}) 132 | fmt.Println([]string{"1"} == []string{"1"}) 133 | } 134 | ``` 135 | 136 | 数组只能与相同纬度长度以及类型的其他数组比较,切片之间不能直接比较。。 137 | 138 | ## 下面代码写法有什么问题? 139 | 140 | ```go 141 | package main 142 | import ( 143 | "fmt" 144 | ) 145 | type Student struct { 146 | Age int 147 | } 148 | func main() { 149 | kv := map[string]Student{"menglu": {Age: 21}} 150 | kv["menglu"].Age = 22 151 | s := []Student{{Age: 21}} 152 | s[0].Age = 22 153 | fmt.Println(kv, s) 154 | } 155 | ``` 156 | 157 | golang中的`map` 通过`key`获取到的实际上是两个值,第一个是获取到的值,第二个是是否存在该`key`。因此不能直接通过`key`来赋值对象。 158 | -------------------------------------------------------------------------------- /question/q015.md: -------------------------------------------------------------------------------- 1 | ## golang 并发题目测试 2 | 3 | 题目来源: [Go并发编程小测验: 你能答对几道题?](https://colobu.com/2019/04/28/go-concurrency-quizzes/) 4 | 5 | ### 1 Mutex 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | "sync" 12 | ) 13 | var mu sync.Mutex 14 | var chain string 15 | func main() { 16 | chain = "main" 17 | A() 18 | fmt.Println(chain) 19 | } 20 | func A() { 21 | mu.Lock() 22 | defer mu.Unlock() 23 | chain = chain + " --> A" 24 | B() 25 | } 26 | func B() { 27 | chain = chain + " --> B" 28 | C() 29 | } 30 | func C() { 31 | mu.Lock() 32 | defer mu.Unlock() 33 | chain = chain + " --> C" 34 | } 35 | ``` 36 | 37 | - A: 不能编译 38 | - B: 输出 main --> A --> B --> C 39 | - C: 输出 main 40 | - D: panic 41 | 42 | ### 2 RWMutex 43 | 44 | ```go 45 | package main 46 | import ( 47 | "fmt" 48 | "sync" 49 | "time" 50 | ) 51 | var mu sync.RWMutex 52 | var count int 53 | func main() { 54 | go A() 55 | time.Sleep(2 * time.Second) 56 | mu.Lock() 57 | defer mu.Unlock() 58 | count++ 59 | fmt.Println(count) 60 | } 61 | func A() { 62 | mu.RLock() 63 | defer mu.RUnlock() 64 | B() 65 | } 66 | func B() { 67 | time.Sleep(5 * time.Second) 68 | C() 69 | } 70 | func C() { 71 | mu.RLock() 72 | defer mu.RUnlock() 73 | } 74 | ``` 75 | 76 | - A: 不能编译 77 | - B: 输出 1 78 | - C: 程序hang住 79 | - D: panic 80 | 81 | ### 3 Waitgroup 82 | 83 | ```go 84 | package main 85 | import ( 86 | "sync" 87 | "time" 88 | ) 89 | func main() { 90 | var wg sync.WaitGroup 91 | wg.Add(1) 92 | go func() { 93 | time.Sleep(time.Millisecond) 94 | wg.Done() 95 | wg.Add(1) 96 | }() 97 | wg.Wait() 98 | } 99 | ``` 100 | 101 | - A: 不能编译 102 | - B: 无输出,正常退出 103 | - C: 程序hang住 104 | - D: panic 105 | 106 | ### 4 双检查实现单例 107 | 108 | ```go 109 | package doublecheck 110 | import ( 111 | "sync" 112 | ) 113 | type Once struct { 114 | m sync.Mutex 115 | done uint32 116 | } 117 | func (o *Once) Do(f func()) { 118 | if o.done == 1 { 119 | return 120 | } 121 | o.m.Lock() 122 | defer o.m.Unlock() 123 | if o.done == 0 { 124 | o.done = 1 125 | f() 126 | } 127 | } 128 | ``` 129 | 130 | - A: 不能编译 131 | - B: 可以编译,正确实现了单例 132 | - C: 可以编译,有并发问题,f函数可能会被执行多次 133 | - D: 可以编译,但是程序运行会panic 134 | 135 | ### 5 Mutex 136 | 137 | ```go 138 | package main 139 | import ( 140 | "fmt" 141 | "sync" 142 | ) 143 | type MyMutex struct { 144 | count int 145 | sync.Mutex 146 | } 147 | func main() { 148 | var mu MyMutex 149 | mu.Lock() 150 | var mu2 = mu 151 | mu.count++ 152 | mu.Unlock() 153 | mu2.Lock() 154 | mu2.count++ 155 | mu2.Unlock() 156 | fmt.Println(mu.count, mu2.count) 157 | } 158 | ``` 159 | 160 | - A: 不能编译 161 | - B: 输出 1, 1 162 | - C: 输出 1, 2 163 | - D: panic 164 | 165 | ### 6 Pool 166 | 167 | ```go 168 | package main 169 | import ( 170 | "bytes" 171 | "fmt" 172 | "runtime" 173 | "sync" 174 | "time" 175 | ) 176 | var pool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} 177 | func main() { 178 | go func() { 179 | for { 180 | processRequest(1 << 28) // 256MiB 181 | } 182 | }() 183 | for i := 0; i < 1000; i++ { 184 | go func() { 185 | for { 186 | processRequest(1 << 10) // 1KiB 187 | } 188 | }() 189 | } 190 | var stats runtime.MemStats 191 | for i := 0; ; i++ { 192 | runtime.ReadMemStats(&stats) 193 | fmt.Printf("Cycle %d: %dB\n", i, stats.Alloc) 194 | time.Sleep(time.Second) 195 | runtime.GC() 196 | } 197 | } 198 | func processRequest(size int) { 199 | b := pool.Get().(*bytes.Buffer) 200 | time.Sleep(500 * time.Millisecond) 201 | b.Grow(size) 202 | pool.Put(b) 203 | time.Sleep(1 * time.Millisecond) 204 | } 205 | ``` 206 | 207 | - A: 不能编译 208 | - B: 可以编译,运行时正常,内存稳定 209 | - C: 可以编译,运行时内存可能暴涨 210 | - D: 可以编译,运行时内存先暴涨,但是过一会会回收掉 211 | 212 | ### 7 channel 213 | 214 | ```go 215 | package main 216 | import ( 217 | "fmt" 218 | "runtime" 219 | "time" 220 | ) 221 | func main() { 222 | var ch chan int 223 | go func() { 224 | ch = make(chan int, 1) 225 | ch <- 1 226 | }() 227 | go func(ch chan int) { 228 | time.Sleep(time.Second) 229 | <-ch 230 | }(ch) 231 | c := time.Tick(1 * time.Second) 232 | for range c { 233 | fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine()) 234 | } 235 | } 236 | ``` 237 | 238 | - A: 不能编译 239 | - B: 一段时间后总是输出 `#goroutines: 1` 240 | - C: 一段时间后总是输出 `#goroutines: 2` 241 | - D: panic 242 | 243 | ### 8 channel 244 | 245 | ```go 246 | package main 247 | import "fmt" 248 | func main() { 249 | var ch chan int 250 | var count int 251 | go func() { 252 | ch <- 1 253 | }() 254 | go func() { 255 | count++ 256 | close(ch) 257 | }() 258 | <-ch 259 | fmt.Println(count) 260 | } 261 | ``` 262 | 263 | - A: 不能编译 264 | - B: 输出 1 265 | - C: 输出 0 266 | - D: panic 267 | 268 | ### 9 Map 269 | 270 | ```go 271 | package main 272 | import ( 273 | "fmt" 274 | "sync" 275 | ) 276 | func main() { 277 | var m sync.Map 278 | m.LoadOrStore("a", 1) 279 | m.Delete("a") 280 | fmt.Println(m.Len()) 281 | } 282 | ``` 283 | 284 | - A: 不能编译 285 | - B: 输出 1 286 | - C: 输出 0 287 | - D: panic 288 | 289 | ### 10 happens before 290 | 291 | ```go 292 | package main 293 | var c = make(chan int) 294 | var a int 295 | func f() { 296 | a = 1 297 | <-c 298 | } 299 | func main() { 300 | go f() 301 | c <- 0 302 | print(a) 303 | } 304 | ``` 305 | 306 | - A: 不能编译 307 | - B: 输出 1 308 | - C: 输出 0 309 | - D: panic 310 | 311 | 312 | 313 | 314 | 315 | ## 答案 316 | 317 | ### 1. D 318 | 319 | 会产生死锁`panic`,因为`Mutex` 是互斥锁。 320 | 321 | ### 2. D 322 | 323 | 会产生死锁`panic`,根据`sync/rwmutex.go` 中注释可以知道,读写锁当有一个协程在等待写锁时,其他协程是不能获得读锁的,而在`A`和`C`中同一个调用链中间需要让出读锁,让写锁优先获取,而`A`的读锁又要求`C`调用完成,因此死锁。 324 | 325 | 326 | ### 3. D 327 | 328 | `WaitGroup` 在调用 `Wait` 之后是不能再调用 `Add` 方法的。 329 | 330 | ### 4. C 331 | 332 | 在多核CPU中,因为CPU缓存会导致多个核心中变量值不同步。 333 | 334 | ### 5. D 335 | 336 | 加锁后复制变量,会将锁的状态也复制,所以`mu1` 其实是已经加锁状态,再加锁会死锁。 337 | 338 | ### 6. C 339 | 340 | 个人理解,在单核CPU中,内存可能会稳定在`256MB`,如果是多核可能会暴涨。 341 | 342 | ### 7. C 343 | 344 | 因为 `ch` 未初始化,写和读都会阻塞,之后被第一个协程重新赋值,导致写的`ch` 都阻塞。 345 | 346 | ### 8. D 347 | 348 | `ch` 未有被初始化,关闭时会报错。 349 | 350 | ### 9. A 351 | 352 | `sync.Map` 没有 `Len` 方法。 353 | 354 | ### 10. B 355 | 356 | `c <- 0` 会阻塞依赖于 `f()` 的执行。 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /question/q016.md: -------------------------------------------------------------------------------- 1 | # 记一道字节跳动的算法面试题 2 | 3 | ## 题目 4 | 5 | 这其实是一道变形的链表反转题,大致描述如下 6 | 给定一个单链表的头节点 head,实现一个调整单链表的函数,使得每K个节点之间为一组进行逆序,并且从链表的尾部开始组起,头部剩余节点数量不够一组的不需要逆序。(不能使用队列或者栈作为辅助) 7 | 8 | **例如:** 9 | 10 | 链表:`1->2->3->4->5->6->7->8->null, K = 3`。那么 `6->7->8`,`3->4->5`,`1->2`各位一组。调整后:`1->2->5->4->3->8->7->6->null`。其中 1,2不调整,因为不够一组。 11 | 12 | **解析** 13 | 14 | 原文: -------------------------------------------------------------------------------- /question/q017.md: -------------------------------------------------------------------------------- 1 | # 多协程查询切片问题 2 | 3 | ## 题目 4 | 5 | 假设有一个超长的切片,切片的元素类型为int,切片中的元素为乱序排序。限时5秒,使用多个goroutine查找切片中是否存在给定的值,在查找到目标值或者超时后立刻结束所有goroutine的执行。 6 | 7 | 比如,切片 `[23,32,78,43,76,65,345,762,......915,86]`,查找目标值为 345 ,如果切片中存在,则目标值输出`"Found it!"`并立即取消仍在执行查询任务的`goroutine`。 8 | 9 | 如果在超时时间未查到目标值程序,则输出`"Timeout!Not Found"`,同时立即取消仍在执行的查找任务的`goroutine`。 10 | 11 | > 答案: 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /question/q018.md: -------------------------------------------------------------------------------- 1 | # 对已经关闭的的chan进行读写,会怎么样?为什么? 2 | 3 | ## 题目 4 | 5 | 对已经关闭的的 chan 进行读写,会怎么样?为什么? 6 | 7 | ## 回答 8 | 9 | - 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。 10 | - 如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。 11 | - 如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。 12 | - 写已经关闭的 chan 会 panic 13 | 14 | 15 | ## 示例 16 | 17 | ### 1. 写已经关闭的 chan 18 | 19 | ```go 20 | func main(){ 21 | c := make(chan int,3) 22 | close(c) 23 | c <- 1 24 | } 25 | //输出结果 26 | panic: send on closed channel 27 | 28 | goroutine 1 [running] 29 | main.main() 30 | ... 31 | ``` 32 | 33 | - 注意这个 send on closed channel,待会会提到。 34 | 35 | ### 2. 读已经关闭的 chan 36 | 37 | ```go 38 | package main 39 | import "fmt" 40 | 41 | func main() { 42 | fmt.Println("以下是数值的chan") 43 | ci:=make(chan int,3) 44 | ci<-1 45 | close(ci) 46 | num,ok := <- ci 47 | fmt.Printf("读chan的协程结束,num=%v, ok=%v\n",num,ok) 48 | num1,ok1 := <-ci 49 | fmt.Printf("再读chan的协程结束,num=%v, ok=%v\n",num1,ok1) 50 | num2,ok2 := <-ci 51 | fmt.Printf("再再读chan的协程结束,num=%v, ok=%v\n",num2,ok2) 52 | 53 | fmt.Println("以下是字符串chan") 54 | cs := make(chan string,3) 55 | cs <- "aaa" 56 | close(cs) 57 | str,ok := <- cs 58 | fmt.Printf("读chan的协程结束,str=%v, ok=%v\n",str,ok) 59 | str1,ok1 := <-cs 60 | fmt.Printf("再读chan的协程结束,str=%v, ok=%v\n",str1,ok1) 61 | str2,ok2 := <-cs 62 | fmt.Printf("再再读chan的协程结束,str=%v, ok=%v\n",str2,ok2) 63 | 64 | fmt.Println("以下是结构体chan") 65 | type MyStruct struct{ 66 | Name string 67 | } 68 | cstruct := make(chan MyStruct,3) 69 | cstruct <- MyStruct{Name: "haha"} 70 | close(cstruct) 71 | stru,ok := <- cstruct 72 | fmt.Printf("读chan的协程结束,stru=%v, ok=%v\n",stru,ok) 73 | stru1,ok1 := <-cs 74 | fmt.Printf("再读chan的协程结束,stru=%v, ok=%v\n",stru1,ok1) 75 | stru2,ok2 := <-cs 76 | fmt.Printf("再再读chan的协程结束,stru=%v, ok=%v\n",stru2,ok2) 77 | } 78 | 79 | ``` 80 | 81 | 输出结果 82 | 83 | ```bash 84 | 以下是数值的chan 85 | 读chan的协程结束,num=1, ok=true 86 | 再读chan的协程结束,num=0, ok=false 87 | 再再读chan的协程结束,num=0, ok=false 88 | 以下是字符串chan 89 | 读chan的协程结束,str=aaa, ok=true 90 | 再读chan的协程结束,str=, ok=false 91 | 再再读chan的协程结束,str=, ok=false 92 | 以下是结构体chan 93 | 读chan的协程结束,stru={haha}, ok=true 94 | 再读chan的协程结束,stru=, ok=false 95 | 再再读chan的协程结束,stru=, ok=false 96 | ``` 97 | 98 | 99 | ## 多问一句 100 | 101 | ### 1. 为什么写已经关闭的 `chan` 就会 `panic` 呢? 102 | 103 | ```go 104 | //在 src/runtime/chan.go 105 | func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr) bool { 106 | //省略其他 107 | if c.closed != 0 { 108 | unlock(&c.lock) 109 | panic(plainError("send on closed channel")) 110 | } 111 | //省略其他 112 | } 113 | ``` 114 | 115 | - 当 `c.closed != 0` 则为通道关闭,此时执行写,源码提示直接 `panic`,输出的内容就是上面提到的 `"send on closed channel"`。 116 | 117 | ### 2. 为什么读已关闭的 chan 会一直能读到值? 118 | 119 | ```go 120 | func chanrecv(c *hchan,ep unsafe.Pointer,block bool) (selected,received bool) { 121 | //省略部分逻辑 122 | lock(&c.lock) 123 | //当chan被关闭了,而且缓存为空时 124 | //ep 是指 val,ok := <-c 里的val地址 125 | if c.closed != 0 && c.qcount == 0 { 126 | if receenabled { 127 | raceacquire(c.raceaddr()) 128 | } 129 | unlock(&c.lock) 130 | //如果接受之的地址不空,那接收值将获得一个该值类型的零值 131 | //typedmemclr 会根据类型清理响应的内存 132 | //这就解释了上面代码为什么关闭的chan 会返回对应类型的零值 133 | if ep != null { 134 | typedmemclr(c.elemtype,ep) 135 | } 136 | //返回两个参数 selected,received 137 | // 第二个采纳数就是 val,ok := <- c 里的 ok 138 | //也就解释了为什么读关闭的chan会一直返回false 139 | return true,false 140 | } 141 | } 142 | ``` 143 | - `c.closed != 0 && c.qcount == 0` 指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值) 144 | - 如果接收值的地址 `ep` 不为空 145 | - 那接收值将获得是一个该类型的零值 146 | - `typedmemclr` 会根据类型清理相应地址的内存 147 | - 这就解释了上面代码为什么关闭的 chan 会返回对应类型的零值 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /question/q019.md: -------------------------------------------------------------------------------- 1 | # 简单聊聊内存逃逸? 2 | 3 | ## 问题 4 | 5 | 知道golang的内存逃逸吗?什么情况下会发生内存逃逸? 6 | 7 | ## 回答 8 | 9 | golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。 10 | 11 | 能引起变量逃逸到堆上的典型情况: 12 | 13 | - **在方法内把局部变量指针返回** 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。 14 | - **发送指针或带有指针的值到 channel 中。** 在编译时,是没有办法知道哪个 `goroutine` 会在 `channel` 上接收数据。所以编译器没法知道变量什么时候才会被释放。 15 | - **在一个切片上存储指针或带指针的值。** 一个典型的例子就是 `[]*string` 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。 16 | - **slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。** slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。 17 | - **在 interface 类型上调用方法。** 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。 18 | 19 | ## 举例 20 | 21 | **通过一个例子加深理解,接下来尝试下怎么通过 `go build -gcflags=-m` 查看逃逸的情况。** 22 | 23 | ```go 24 | package main 25 | import "fmt" 26 | type A struct { 27 | s string 28 | } 29 | // 这是上面提到的 "在方法内把局部变量指针返回" 的情况 30 | func foo(s string) *A { 31 | a := new(A) 32 | a.s = s 33 | return a //返回局部变量a,在C语言中妥妥野指针,但在go则ok,但a会逃逸到堆 34 | } 35 | func main() { 36 | a := foo("hello") 37 | b := a.s + " world" 38 | c := b + "!" 39 | fmt.Println(c) 40 | } 41 | ``` 42 | 43 | 执行go build -gcflags=-m main.go 44 | 45 | ```bash 46 | go build -gcflags=-m main.go 47 | # command-line-arguments 48 | ./main.go:7:6: can inline foo 49 | ./main.go:13:10: inlining call to foo 50 | ./main.go:16:13: inlining call to fmt.Println 51 | /var/folders/45/qx9lfw2s2zzgvhzg3mtzkwzc0000gn/T/go-build409982591/b001/_gomod_.go:6:6: can inline init.0 52 | ./main.go:7:10: leaking param: s 53 | ./main.go:8:10: new(A) escapes to heap 54 | ./main.go:16:13: io.Writer(os.Stdout) escapes to heap 55 | ./main.go:16:13: c escapes to heap 56 | ./main.go:15:9: b + "!" escapes to heap 57 | ./main.go:13:10: main new(A) does not escape 58 | ./main.go:14:11: main a.s + " world" does not escape 59 | ./main.go:16:13: main []interface {} literal does not escape 60 | :1: os.(*File).close .this does not escape 61 | ``` 62 | 63 | - `./main.go:8:10: new(A) escapes to heap` 说明 `new(A)` 逃逸了,符合上述提到的常见情况中的第一种。 64 | - `./main.go:14:11: main a.s + " world" does not escape` 说明 b 变量没有逃逸,因为它只在方法内存在,会在方法结束时被回收。 65 | - `./main.go:15:9: b + "!" escapes to heap` 说明 c 变量逃逸,通过`fmt.Println(a ...interface{})`打印的变量,都会发生逃逸,感兴趣的朋友可以去查查为什么。 66 | 67 | 以上操作其实就叫逃逸分析。下篇文章,跟大家聊聊怎么用一个比较trick的方法使变量不逃逸。方便大家在面试官面前秀一波。 68 | 69 | 70 | >原文 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /question/q020.md: -------------------------------------------------------------------------------- 1 | # 字符串转成byte数组,会发生内存拷贝吗? 2 | 3 | ## 问题 4 | 5 | 字符串转成byte数组,会发生内存拷贝吗? 6 | 7 | ## 回答 8 | 9 | 字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。那么问题来了。 10 | 11 | 频繁的内存拷贝操作听起来对性能不大友好。有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢? 12 | 13 | ## 解释 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "reflect" 21 | "unsafe" 22 | ) 23 | 24 | func main() { 25 | a :="aaa" 26 | ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a)) 27 | b := *(*[]byte)(unsafe.Pointer(&ssh)) 28 | fmt.Printf("%v",b) 29 | } 30 | ``` 31 | 32 | **`StringHeader` 是字符串在go的底层结构。** 33 | 34 | ```go 35 | type StringHeader struct { 36 | Data uintptr 37 | Len int 38 | } 39 | ``` 40 | 41 | **`SliceHeader` 是切片在go的底层结构。** 42 | 43 | ```go 44 | type SliceHeader struct { 45 | Data uintptr 46 | Len int 47 | Cap int 48 | } 49 | ``` 50 | 51 | 那么如果想要在底层转换二者,只需要把 StringHeader 的地址强转成 SliceHeader 就行。那么go有个很强的包叫 unsafe 。 52 | 53 | 1. `unsafe.Pointer(&a)`方法可以得到变量a的地址。 54 | 2. `(*reflect.StringHeader)(unsafe.Pointer(&a))` 可以把字符串a转成底层结构的形式。 55 | 3. `(*[]byte)(unsafe.Pointer(&ssh))` 可以把ssh底层结构体转成byte的切片的指针。 56 | 4. 再通过 `*`转为指针指向的实际内容。 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /question/q021.md: -------------------------------------------------------------------------------- 1 | # http包的内存泄漏 2 | 3 | ## 问题 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "io/ioutil" 11 | "net/http" 12 | "runtime" 13 | ) 14 | 15 | func main() { 16 | num := 6 17 | for index := 0; index < num; index++ { 18 | resp, _ := http.Get("https://www.baidu.com") 19 | _, _ = ioutil.ReadAll(resp.Body) 20 | } 21 | fmt.Printf("此时goroutine个数= %d\n", runtime.NumGoroutine()) 22 | 23 | 24 | } 25 | ``` 26 | 27 | 上面这道题在不执行`resp.Body.Close()`的情况下,泄漏了吗?如果泄漏,泄漏了多少个goroutine? 28 | 29 | ## 怎么答 30 | 31 | 不进行resp.Body.Close(),泄漏是一定的。但是泄漏的goroutine个数就让我迷糊了。由于执行了6遍,每次泄漏一个读和写goroutine,就是12个goroutine,加上main函数本身也是一个goroutine,所以答案是13. 32 | 33 | 然而执行程序,发现答案是3,出入有点大,为什么呢? 34 | 35 | ## 解释 36 | 37 | 我们直接看源码。golang 的 http 包。 38 | 39 | ```go 40 | http.Get() 41 | 42 | -- DefaultClient.Get 43 | ----func (c *Client) do(req *Request) 44 | ------func send(ireq *Request, rt RoundTripper, deadline time.Time) 45 | -------- resp, didTimeout, err = send(req, c.transport(), deadline) 46 | // 以上代码在 go/1.12.7/libexec/src/net/http/client:174 47 | 48 | func (c *Client) transport() RoundTripper { 49 | if c.Transport != nil { 50 | return c.Transport 51 | } 52 | return DefaultTransport 53 | } 54 | ``` 55 | 56 | - 说明 `http.Get` 默认使用 `DefaultTransport` 管理连接。 57 | 58 | DefaultTransport 是干嘛的呢? 59 | 60 | ```go 61 | // It establishes network connections as needed 62 | // and caches them for reuse by subsequent calls. 63 | ``` 64 | 65 | - `DefaultTransport` 的作用是根据需要建立网络连接并缓存它们以供后续调用重用。 66 | 67 | 那么 `DefaultTransport` 什么时候会建立连接呢? 68 | 69 | 接着上面的代码堆栈往下翻 70 | 71 | ```go 72 | func send(ireq *Request, rt RoundTripper, deadline time.Time) 73 | --resp, err = rt.RoundTrip(req) // 以上代码在 go/1.12.7/libexec/src/net/http/client:250 74 | func (t *Transport) RoundTrip(req *http.Request) 75 | func (t *Transport) roundTrip(req *Request) 76 | func (t *Transport) getConn(treq *transportRequest, cm connectMethod) 77 | func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) { 78 | ... 79 | go pconn.readLoop() // 启动一个读goroutine 80 | go pconn.writeLoop() // 启动一个写goroutine 81 | return pconn, nil 82 | } 83 | ``` 84 | 85 | - 一次建立连接,就会启动一个读goroutine和写goroutine。这就是为什么一次`http.Get()`会泄漏两个goroutine的来源。 86 | - 泄漏的来源知道了,也知道是因为没有执行close 87 | 88 | **那为什么不执行 close 会泄漏呢?** 89 | 90 | 回到刚刚启动的读goroutine 的 `readLoop()` 代码里 91 | 92 | ```go 93 | func (pc *persistConn) readLoop() { 94 | alive := true 95 | for alive { 96 | ... 97 | // Before looping back to the top of this function and peeking on 98 | // the bufio.Reader, wait for the caller goroutine to finish 99 | // reading the response body. (or for cancelation or death) 100 | select { 101 | case bodyEOF := <-waitForBodyRead: 102 | pc.t.setReqCanceler(rc.req, nil) // before pc might return to idle pool 103 | alive = alive && 104 | bodyEOF && 105 | !pc.sawEOF && 106 | pc.wroteRequest() && 107 | tryPutIdleConn(trace) 108 | if bodyEOF { 109 | eofc <- struct{}{} 110 | } 111 | case <-rc.req.Cancel: 112 | alive = false 113 | pc.t.CancelRequest(rc.req) 114 | case <-rc.req.Context().Done(): 115 | alive = false 116 | pc.t.cancelRequest(rc.req, rc.req.Context().Err()) 117 | case <-pc.closech: 118 | alive = false 119 | } 120 | ... 121 | } 122 | } 123 | ``` 124 | 125 | 其中第一个 body 被读取完或关闭这个 case: 126 | 127 | ```go 128 | alive = alive && 129 | bodyEOF && 130 | !pc.sawEOF && 131 | pc.wroteRequest() && 132 | tryPutIdleConn(trace) 133 | 134 | ``` 135 | 136 | bodyEOF 来源于到一个通道 waitForBodyRead,这个字段的 true 和 false 直接决定了 alive 变量的值(alive=true那读goroutine继续活着,循环,否则退出goroutine)。 137 | 138 | **那么这个通道的值是从哪里过来的呢?** 139 | 140 | 141 | ```go 142 | // go/1.12.7/libexec/src/net/http/transport.go: 1758 143 | body := &bodyEOFSignal{ 144 | body: resp.Body, 145 | earlyCloseFn: func() error { 146 | waitForBodyRead <- false 147 | <-eofc // will be closed by deferred call at the end of the function 148 | return nil 149 | 150 | }, 151 | fn: func(err error) error { 152 | isEOF := err == io.EOF 153 | waitForBodyRead <- isEOF 154 | if isEOF { 155 | <-eofc // see comment above eofc declaration 156 | } else if err != nil { 157 | if cerr := pc.canceled(); cerr != nil { 158 | return cerr 159 | } 160 | } 161 | return err 162 | }, 163 | } 164 | ``` 165 | 166 | - 如果执行 earlyCloseFn ,waitForBodyRead 通道输入的是 false,alive 也会是 false,那 readLoop() 这个 goroutine 就会退出。 167 | - 如果执行 fn ,其中包括正常情况下 body 读完数据抛出 io.EOF 时的 case,waitForBodyRead 通道输入的是 true,那 alive 会是 true,那么 readLoop() 这个 goroutine 就不会退出,同时还顺便执行了 tryPutIdleConn(trace) 。 168 | 169 | ```go 170 | // tryPutIdleConn adds pconn to the list of idle persistent connections awaiting 171 | // a new request. 172 | // If pconn is no longer needed or not in a good state, tryPutIdleConn returns 173 | // an error explaining why it wasn't registered. 174 | // tryPutIdleConn does not close pconn. Use putOrCloseIdleConn instead for that. 175 | func (t *Transport) tryPutIdleConn(pconn *persistConn) error 176 | ``` 177 | 178 | - tryPutIdleConn 将 pconn 添加到等待新请求的空闲持久连接列表中,也就是之前说的连接会复用。 179 | 180 | 那么问题又来了,什么时候会执行这个 `fn` 和 `earlyCloseFn` 呢? 181 | 182 | ```go 183 | func (es *bodyEOFSignal) Close() error { 184 | es.mu.Lock() 185 | defer es.mu.Unlock() 186 | if es.closed { 187 | return nil 188 | } 189 | es.closed = true 190 | if es.earlyCloseFn != nil && es.rerr != io.EOF { 191 | return es.earlyCloseFn() // 关闭时执行 earlyCloseFn 192 | } 193 | err := es.body.Close() 194 | return es.condfn(err) 195 | } 196 | ``` 197 | 198 | - 上面这个其实就是我们比较收悉的 resp.Body.Close() ,在里面会执行 earlyCloseFn,也就是此时 readLoop() 里的 waitForBodyRead 通道输入的是 false,alive 也会是 false,那 readLoop() 这个 goroutine 就会退出,goroutine 不会泄露。 199 | 200 | ```go 201 | b, err = ioutil.ReadAll(resp.Body) 202 | --func ReadAll(r io.Reader) 203 | ----func readAll(r io.Reader, capacity int64) 204 | ------func (b *Buffer) ReadFrom(r io.Reader) 205 | 206 | 207 | // go/1.12.7/libexec/src/bytes/buffer.go:207 208 | func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) { 209 | for { 210 | ... 211 | m, e := r.Read(b.buf[i:cap(b.buf)]) // 看这里,是body在执行read方法 212 | ... 213 | } 214 | } 215 | ``` 216 | 217 | - 这个`read`,其实就是 `bodyEOFSignal` 里的 218 | 219 | ```go 220 | func (es *bodyEOFSignal) Read(p []byte) (n int, err error) { 221 | ... 222 | n, err = es.body.Read(p) 223 | if err != nil { 224 | ... 225 | // 这里会有一个io.EOF的报错,意思是读完了 226 | err = es.condfn(err) 227 | } 228 | return 229 | } 230 | 231 | 232 | func (es *bodyEOFSignal) condfn(err error) error { 233 | if es.fn == nil { 234 | return err 235 | } 236 | err = es.fn(err) // 这了执行了 fn 237 | es.fn = nil 238 | return err 239 | } 240 | ``` 241 | 242 | - 上面这个其实就是我们比较收悉的读取 body 里的内容。 ioutil.ReadAll() ,在读完 body 的内容时会执行 fn,也就是此时 readLoop() 里的 waitForBodyRead 通道输入的是 true,alive 也会是 true,那 readLoop() 这个 goroutine 就不会退出,goroutine 会泄露,然后执行 tryPutIdleConn(trace) 把连接放回池子里复用。 243 | 244 | ## 总结 245 | 246 | - 所以结论呼之欲出了,虽然执行了 6 次循环,而且每次都没有执行 Body.Close() ,就是因为执行了ioutil.ReadAll()把内容都读出来了,连接得以复用,因此只泄漏了一个读goroutine和一个写goroutine,最后加上main goroutine,所以答案就是3个goroutine。 247 | - 从另外一个角度说,正常情况下我们的代码都会执行 ioutil.ReadAll(),但如果此时忘了 resp.Body.Close(),确实会导致泄漏。但如果你调用的域名一直是同一个的话,那么只会泄漏一个 读goroutine 和一个写goroutine,这就是为什么代码明明不规范但却看不到明显内存泄漏的原因。 248 | - 那么问题又来了,为什么上面要特意强调是同一个域名呢?改天,回头,以后有空再说吧。 249 | 250 | 251 | >作者:9號同学 252 | 链接:https://juejin.cn/post/6896993332019822605 253 | 来源:掘金 254 | 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 255 | 256 | 257 | 作者:9號同学 258 | 链接:https://juejin.cn/post/6896993332019822605 259 | 来源:掘金 260 | 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 261 | 262 | 263 | -------------------------------------------------------------------------------- /question/q022.md: -------------------------------------------------------------------------------- 1 | # sync.Map 的用法 2 | 3 | ## 问题 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "sync" 11 | ) 12 | 13 | func main(){ 14 | var m sync.Map 15 | m.Store("address",map[string]string{"province":"江苏","city":"南京"}) 16 | v,_ := m.Load("address") 17 | fmt.Println(v["province"]) 18 | } 19 | ``` 20 | 21 | - A,江苏; 22 | - B`,v["province"]`取值错误; 23 | - C,`m.Store`存储错误; 24 | - D,不知道 25 | 26 | ## 解析 27 | 28 | `invalid operation: v["province"] (type interface {} does not support indexing)` 29 | 因为 `func (m *Map) Store(key interface{}, value interface{})` 30 | 所以 `v`类型是 `interface {}` ,这里需要一个类型断言 31 | 32 | ```go 33 | fmt.Println(v.(map[string]string)["province"]) //江苏 34 | ``` 35 | -------------------------------------------------------------------------------- /question/q023.md: -------------------------------------------------------------------------------- 1 | # Golang基础语法相关题目 2 | 3 | ## 1. 写出下面代码输出内容。 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | func main() { 13 | defer_call() 14 | } 15 | 16 | func defer_call() { 17 | defer func() { fmt.Println("打印前") }() 18 | defer func() { fmt.Println("打印中") }() 19 | defer func() { fmt.Println("打印后") }() 20 | 21 | panic("触发异常") 22 | } 23 | ``` 24 | 25 | **考点:** defer执行顺序 26 | 27 | **解答:** defer 是后进先出。 panic 需要等defer 结束后才会向上传递。 28 | 29 | 出现panic恐慌时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。 30 | 31 | ```text 32 | 打印后 33 | 打印中 34 | 打印前 35 | panic: 触发异常 36 | ``` 37 | 38 | ## 2. 以下代码有什么问题,说明原因。 39 | 40 | ```go 41 | type student struct { 42 | Name string 43 | Age int 44 | } 45 | 46 | func pase_student() { 47 | m := make(map[string]*student) 48 | stus := []student{ 49 | {Name: "zhou", Age: 24}, 50 | {Name: "li", Age: 23}, 51 | {Name: "wang", Age: 22}, 52 | } 53 | for _, stu := range stus { 54 | m[stu.Name] = &stu 55 | } 56 | 57 | } 58 | ``` 59 | 60 | **考点:** foreach 61 | 62 | **解答: ** 这样的写法初学者经常会遇到的,很危险! 与Java的foreach一样,都是使用副本的方式。 63 | 64 | 所以`m[stu.Name]=&stu`实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct的值拷贝。 就像想修改切片元素的属性: 65 | 66 | ```go 67 | for _, stu := range stus { 68 | stu.Age = stu.Age+10 69 | } 70 | ``` 71 | 72 | 也是不可行的。 大家可以试试打印出来: 73 | 74 | ```go 75 | func pase_student() { 76 | m := make(map[string]*student) 77 | stus := []student{ 78 | {Name: "zhou", Age: 24}, 79 | {Name: "li", Age: 23}, 80 | {Name: "wang", Age: 22}, 81 | } 82 | // 错误写法 83 | for _, stu := range stus { 84 | m[stu.Name] = &stu 85 | } 86 | 87 | for k,v:=range m{ 88 | println(k,"=>",v.Name) 89 | } 90 | 91 | // 正确 92 | for i:=0;i",v.Name) 97 | } 98 | } 99 | ``` 100 | 101 | ## 3. 下面的代码会输出什么,并说明原因 102 | 103 | ```go 104 | func main() { 105 | runtime.GOMAXPROCS(1) 106 | wg := sync.WaitGroup{} 107 | wg.Add(20) 108 | for i := 0; i < 10; i++ { 109 | go func() { 110 | fmt.Println("A: ", i) 111 | wg.Done() 112 | }() 113 | } 114 | for i := 0; i < 10; i++ { 115 | go func(i int) { 116 | fmt.Println("B: ", i) 117 | wg.Done() 118 | }(i) 119 | } 120 | wg.Wait() 121 | } 122 | ``` 123 | 124 | **考点:** go执行的随机性和闭包 125 | 126 | **解答:** 谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 127 | 128 | 但是`A:`均为输出`10`,`B:`从0~9输出(顺序不定)。 第一个`go func`中`i`是外部`for`的一个变量,地址不变化。遍历完成后,最终`i=10`。 129 | 130 | 故`go func`执行时,i的值始终是10。 131 | 132 | 第二个`go func`中i是函数参数,与外部for中的i完全是两个变量。 尾部(i)将发生值拷贝,go func内部指向值拷贝地址。 133 | 134 | ## 4. 下面代码会输出什么? 135 | 136 | ```go 137 | type People struct{} 138 | 139 | func (p *People) ShowA() { 140 | fmt.Println("showA") 141 | p.ShowB() 142 | } 143 | func (p *People) ShowB() { 144 | fmt.Println("showB") 145 | } 146 | 147 | type Teacher struct { 148 | People 149 | } 150 | 151 | func (t *Teacher) ShowB() { 152 | fmt.Println("teacher showB") 153 | } 154 | 155 | func main() { 156 | t := Teacher{} 157 | t.ShowA() 158 | } 159 | ``` 160 | 161 | **考点:** go的组合继承 162 | 163 | **解答: ** 这是Golang的组合模式,可以实现OOP的继承。 164 | 165 | 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 166 | 167 | 此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能。 168 | 169 | ```text 170 | showA 171 | showB 172 | ``` 173 | 174 | ## 5. 下面代码会触发异常吗?请详细说明 175 | 176 | ```go 177 | func main() { 178 | runtime.GOMAXPROCS(1) 179 | int_chan := make(chan int, 1) 180 | string_chan := make(chan string, 1) 181 | int_chan <- 1 182 | string_chan <- "hello" 183 | select { 184 | case value := <-int_chan: 185 | fmt.Println(value) 186 | case value := <-string_chan: 187 | panic(value) 188 | } 189 | } 190 | ``` 191 | 192 | **考点:** select随机性 193 | 194 | **解答: ** select会随机选择一个可用通用做收发操作。 所以代码是有肯触发异常,也有可能不会。 195 | 196 | 单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则: 197 | 198 | * select 中只要有一个case能return,则立刻执行。 199 | * 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。 200 | * 如果没有一个case能return则可以执行”default”块。 201 | 202 | ## 6. 下面代码输出什么? 203 | 204 | ```go 205 | func calc(index string, a, b int) int { 206 | ret := a + b 207 | fmt.Println(index, a, b, ret) 208 | return ret 209 | } 210 | 211 | func main() { 212 | a := 1 213 | b := 2 214 | defer calc("1", a, calc("10", a, b)) 215 | a = 0 216 | defer calc("2", a, calc("20", a, b)) 217 | b = 1 218 | } 219 | ``` 220 | 221 | **考点:** defer执行顺序 222 | 223 | **解答:** 这道题类似第1题 需要注意到defer执行顺序和值传递 `index:1`肯定是最后执行的,但是`index:1`的第三个参数是一个函数,所以最先被调用`calc("10",1,2)==>10,1,2,3` 执行`index:2`时,与之前一样,需要先调用 `calc("20",0,2)==>20,0,2,2` 执行到`b=1`时候开始调用,`index:2==>calc("2",0,2)==>2,0,2,2 `最后执行`index:1==>calc("1",1,3)==>1,1,3,4` 224 | 225 | ```text 226 | 10 1 2 3 227 | 20 0 2 2 228 | 2 0 2 2 229 | 1 1 3 4 230 | ``` 231 | 232 | ## 7. 请写出以下输入内容 233 | 234 | ```go 235 | func main() { 236 | s := make([]int, 0) 237 | s = append(s, 1, 2, 3) 238 | fmt.Println(s) 239 | } 240 | ``` 241 | 242 | **考点:** make默认值和append 243 | 244 | **解答:** make初始化是由默认值的哦,此处默认值为0 245 | 246 | ```text 247 | [0 0 0 0 0 1 2 3] 248 | ``` 249 | 250 | 大家试试改为: 251 | 252 | ```go 253 | s := make([]int, 0) 254 | s = append(s, 1, 2, 3) 255 | fmt.Println(s)//[1 2 3] 256 | ``` 257 | 258 | ## 8. 下面的代码有什么问题? 259 | 260 | ```go 261 | type UserAges struct { 262 | ages map[string]int 263 | sync.Mutex 264 | } 265 | 266 | func (ua *UserAges) Add(name string, age int) { 267 | ua.Lock() 268 | defer ua.Unlock() 269 | ua.ages[name] = age 270 | } 271 | 272 | func (ua *UserAges) Get(name string) int { 273 | if age, ok := ua.ages[name]; ok { 274 | return age 275 | } 276 | return -1 277 | } 278 | ``` 279 | 280 | **考点:** map线程安全 281 | 282 | **解答:** 可能会出现fatal error: concurrent map read and map write. 修改一下看看效果 283 | 284 | ```go 285 | func (ua *UserAges) Get(name string) int { 286 | ua.Lock() 287 | defer ua.Unlock() 288 | if age, ok := ua.ages[name]; ok { 289 | return age 290 | } 291 | return -1 292 | } 293 | ``` 294 | 295 | ## 9. 下面的迭代会有什么问题? 296 | 297 | ```go 298 | func (set *threadSafeSet) Iter() <-chan interface{} { 299 | ch := make(chan interface{}) 300 | go func() { 301 | set.RLock() 302 | 303 | for elem := range set.s { 304 | ch <- elem 305 | } 306 | 307 | close(ch) 308 | set.RUnlock() 309 | 310 | }() 311 | return ch 312 | } 313 | ``` 314 | 315 | **考点:** chan缓存池 316 | 317 | **解答:** 看到这道题,我也在猜想出题者的意图在哪里。 318 | 319 | `chan`?`sync.RWMutex`?`go`?`chan`缓存池?迭代? 所以只能再读一次题目,就从迭代入手看看。 320 | 321 | 既然是迭代就会要求set.s全部可以遍历一次。但是chan是为缓存的,那就代表这写入一次就会阻塞。 我们把代码恢复为可以运行的方式,看看效果 322 | 323 | ```go 324 | package main 325 | 326 | import ( 327 | "sync" 328 | "fmt" 329 | ) 330 | 331 | //下面的迭代会有什么问题? 332 | 333 | type threadSafeSet struct { 334 | sync.RWMutex 335 | s []interface{} 336 | } 337 | 338 | func (set *threadSafeSet) Iter() <-chan interface{} { 339 | // ch := make(chan interface{}) // 解除注释看看! 340 | ch := make(chan interface{},len(set.s)) 341 | go func() { 342 | set.RLock() 343 | 344 | for elem,value := range set.s { 345 | ch <- elem 346 | println("Iter:",elem,value) 347 | } 348 | 349 | close(ch) 350 | set.RUnlock() 351 | 352 | }() 353 | return ch 354 | } 355 | 356 | func main() { 357 | 358 | th:=threadSafeSet{ 359 | s:[]interface{}{"1","2"}, 360 | } 361 | v:=<-th.Iter() 362 | fmt.Sprintf("%s%v","ch",v) 363 | } 364 | ``` 365 | 366 | ## 10. 以下代码能编译过去吗?为什么? 367 | 368 | ```go 369 | package main 370 | 371 | import ( 372 | "fmt" 373 | ) 374 | 375 | type People interface { 376 | Speak(string) string 377 | } 378 | 379 | type Stduent struct{} 380 | 381 | func (stu *Stduent) Speak(think string) (talk string) { 382 | if think == "bitch" { 383 | talk = "You are a good boy" 384 | } else { 385 | talk = "hi" 386 | } 387 | return 388 | } 389 | 390 | func main() { 391 | var peo People = Stduent{} 392 | think := "bitch" 393 | fmt.Println(peo.Speak(think)) 394 | } 395 | ``` 396 | 397 | **考点:** golang的方法集 398 | 399 | **解答:** 编译不通过! 做错了!?说明你对golang的方法集还有一些疑问。 一句话:golang的方法集仅仅影响接口实现和方法表达式转化,与通过实例或者指针调用方法无关。 400 | 401 | ## 11. 以下代码打印出来什么内容,说出为什么。 402 | 403 | ```go 404 | package main 405 | 406 | import ( 407 | "fmt" 408 | ) 409 | 410 | type People interface { 411 | Show() 412 | } 413 | 414 | type Student struct{} 415 | 416 | func (stu *Student) Show() { 417 | 418 | } 419 | 420 | func live() People { 421 | var stu *Student 422 | return stu 423 | } 424 | 425 | func main() { 426 | if live() == nil { 427 | fmt.Println("AAAAAAA") 428 | } else { 429 | fmt.Println("BBBBBBB") 430 | } 431 | } 432 | ``` 433 | 434 | **考点:** interface内部结构 435 | 436 | **解答:** 很经典的题! 这个考点是很多人忽略的interface内部结构。 go中的接口分为两种一种是空的接口类似这样: `var in interface{}` 437 | 438 | 另一种如题目: 439 | 440 | ```go 441 | type People interface { 442 | Show() 443 | } 444 | ``` 445 | 446 | 他们的底层结构如下: 447 | 448 | ```go 449 | type eface struct { //空接口 450 | _type *_type //类型信息 451 | data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*) 452 | } 453 | type iface struct { //带有方法的接口 454 | tab *itab //存储type信息还有结构实现方法的集合 455 | data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*) 456 | } 457 | type _type struct { 458 | size uintptr //类型大小 459 | ptrdata uintptr //前缀持有所有指针的内存大小 460 | hash uint32 //数据hash值 461 | tflag tflag 462 | align uint8 //对齐 463 | fieldalign uint8 //嵌入结构体时的对齐 464 | kind uint8 //kind 有些枚举值kind等于0是无效的 465 | alg *typeAlg //函数指针数组,类型实现的所有方法 466 | gcdata *byte 467 | str nameOff 468 | ptrToThis typeOff 469 | } 470 | type itab struct { 471 | inter *interfacetype //接口类型 472 | _type *_type //结构类型 473 | link *itab 474 | bad int32 475 | inhash int32 476 | fun [1]uintptr //可变大小 方法集合 477 | } 478 | ``` 479 | 480 | 可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口 结果: 481 | 482 | ```text 483 | BBBBBBB 484 | ``` 485 | 486 | 487 | 488 | -------------------------------------------------------------------------------- /question/q024.md: -------------------------------------------------------------------------------- 1 | # go 面试题:连接字符串有几种方法 2 | 3 | 字符串连接是编程中最基本的操作之一,每种语言都有不同的方法来实现,我们接下来研究下 go 能够有几种方式来连接字符串。 4 | 5 | ## + 6 | 7 | 加号(+)运算符可用于连接字符串。它通常是最常用的字符串连接方式,无需太多考虑。 8 | 9 | ```go 10 | name := "John" + " " + "Doe" // John Doe 11 | ``` 12 | 13 | 只要提供多个字符串作为参数,fmt 包中的 print 函数就会自动连接。它还可以在字符串之间添加空格。 14 | 15 | ```go 16 | fmt.Println("It", "works!") // prints "It works!" 17 | ``` 18 | 19 | ## += 20 | 21 | 可以使用 += 运算符将字符串附加到另一个字符串上。和加号运算符一样,但是是一种稍微短一点的连接方式。它将右侧附加到操作它的字符串上。所以,它本质上是附加字符串。下面是使用 += 运算符追加字符串的示例。 22 | 23 | ```go 24 | u := "This" 25 | v := " is working." 26 | u += v // sets u to "This is working." 27 | ``` 28 | 29 | ## strings.join() 30 | 31 | strings 包中的 join 函数可用于连接字符串,如下所示: 32 | 33 | ```go 34 | func Join(a []string, sep string) string 35 | ``` 36 | 37 | 它需要两个参数,一个字符串数组和一个分隔符来连接他们。它将从中生成一个字符串,下面是一个示例: 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | "strings" 45 | ) 46 | 47 | func main() { 48 | 49 | s := []string{"This", "is", "a", "string."} 50 | 51 | v := strings.Join(s, " ") 52 | 53 | fmt.Println(v) // This is a string. 54 | } 55 | ``` 56 | 57 | ## fmt.Sprintf() 58 | 59 | fmt 包中的 Sprintf 方法可用于字符串连接。下面是个例子: 60 | 61 | ```go 62 | "fmt" 63 | ) 64 | 65 | func main() { 66 | 67 | s1 := "abc" 68 | s2 := "xyz" 69 | 70 | v := fmt.Sprintf("%s%s", s1, s2) 71 | 72 | fmt.Println(v) // abcxyz 73 | } 74 | ``` 75 | 76 | 如你所看到的,字符串 format 允许我们以这种方式进行字符串连接。 77 | 78 | ## bytes.buffer 79 | 80 | bytes 包包含一种类型的 buffer,即字节缓冲。我们可以使用 WriteString 方法写入字符串,然后将缓冲转换为字符串。下面是一个示例: 81 | 82 | ```go 83 | package main 84 | 85 | import ( 86 | "fmt" 87 | "bytes" 88 | ) 89 | 90 | func main() { 91 | var b bytes.Buffer 92 | 93 | b.WriteString("abc") 94 | b.WriteString("def") // append 95 | 96 | fmt.Println(b.String()) // abcdef 97 | } 98 | ``` 99 | 100 | ## strings builder 101 | 102 | strings 包有一个 builder 类型,这是构建字符串的一种非常有效的方法。它在连接字符串时使用的内存要少得多,是一种更好的连接方式。函数 WriteString 允许我们以更快的方式连接字符串,下面是示例: 103 | 104 | ```go 105 | package main 106 | 107 | import ( 108 | "fmt" 109 | "strings" 110 | ) 111 | 112 | func main() { 113 | var sb strings.Builder 114 | sb.WriteString("First") 115 | sb.WriteString("Second") 116 | fmt.Println(sb.String()) // FirstSecond 117 | } 118 | ``` 119 | 120 | 使用同一个 builder,我们可以向字符串里面添加 rune 或 bytes。 121 | 122 | ## strings.Repeat 123 | 124 | 我们可以多次连接同一个字符串以形成另一个字符串。strings 包中的 repeat 方法以一种高效的方式完成了这项工作: 125 | 126 | ```go 127 | package main 128 | 129 | import ( 130 | "fmt" 131 | "strings" 132 | ) 133 | 134 | func main() { 135 | fmt.Println(strings.Repeat("abc", 3)) // abcabcabc 136 | } 137 | ``` 138 | 139 | # 思维导图 140 | 141 | ![go-面试题-连接字符串有几种方法.png](../images/go-strings.png) -------------------------------------------------------------------------------- /redis/redis-master-slave.md: -------------------------------------------------------------------------------- 1 | # Redis主从复制原理 2 | 3 | 相信很多小伙伴都已经配置过主从复制,但是对于redis主从复制的工作流程和常见问题很多都没有深入的了解。咔咔这次用时俩天时间给大家整理一份redis主从复制的全部知识点。本文实现所需环境 centos7.0 redis4.0 4 | 5 | ## 一、什么是Redis主从复制? 6 | 7 | 主从复制就是现在有俩台redis服务器,把一台redis的数据同步到另一台redis数据库上。前者称之为主节点(master),后者为从节点(slave)。数据是只能master往slave同步单向。 8 | 9 | 但是在实际过程中是不可能只有俩台redis服务器来做主从复制的,这也就意味这每台redis服务器都有可能会称为主节点(master) 10 | 11 | 下图案例中,我们的slave3既是master的从节点,也是slave的主节点。 12 | 13 | 先知道这么个概念,更多详解继续查看下文。 14 | 15 | ![](../images/af2760bc01cc4da6808cd34087b7176a.jpeg) 16 | 17 | ## 二、为什么需要Redis主从复制? 18 | 19 | 假设我们现在就一台redis服务器,也就是单机状态。 20 | 21 | 在这种情况下会出现的第一个问题就是服务器宕机,直接导致数据丢失。如果项目是跟¥占关系的,那造成的后果就可想而知。 22 | 23 | 第二个情况就是内存问题了,当只有一台服务器时内存肯定会到达峰值的,不可能对一台服务器进行无限升级的。 24 | 25 | ![](../images/b781d9b31ebd4b718de3083cf036fa4b.jpeg) 26 | 27 | 所以针对以上俩个问题,我们就多准备几台服务器,配置主从复制。将数据保存在多个服务器上。并且保证每个服务器的数据是同步的。即使有一个服务器宕机了,也不会影响用户的使用。redis可以继续实现高可用、同时实现数据的冗余备份。 28 | 29 | 这会应该会有很多疑问,master跟slave怎么连接呢? 如何同步数据呢? 假如master服务器宕机了呢?别着急,一点一点解决你的问题。 30 | 31 | ![](../images/6289b6c04279481789a6459dc8da2cb1.jpeg) 32 | 33 | ## 三、Redis主从复制的作用 34 | 35 | 在上边我们说了为什么使用redis的主从复制,那么主从复制的作用就是针对为什么使用它来讲了。 36 | 37 | 我们继续使用这个图来谈论 38 | 39 | - 第一点是数据冗余了,实现了数据的热备份,是持久化之外的另一种方式。 40 | - 第二点是针对单机故障问题。当主节点也就是master出现问题时,可以由从节点来提供服务也就是slave,实现了快速恢复故障,也就是服务冗余。 41 | - 第三点是读写分离,master服务器主要是写,slave主要用来读数据,可以提高服务器的负载能力。同时可以根据需求的变化,添加从节点的数量。 42 | - 第四点是负载均衡,配合读写分离,有主节点提供写服务,从节点提供读服务,分担服务器负载,尤其在写少读多的情况下,通过多个从节点分担读负载,可以大大提高redis服务器的并发量和负载。 43 | - 第五点是高可用的基石,主从复制是哨兵和集群能够实施的基础,因此我们可以说主从复制是高可用的基石。 44 | 45 | ![](../images/11b30c167de447ffad6d1ab741f921f6.jpeg) 46 | 47 | ## 四、配置Redis主从复制 48 | 说了这么多,我们先简单的配置一个主从复制案例,然后在谈实现的原理。 49 | 50 | redis存储路径为:usr/local/redis 51 | 52 | 日志跟配置文件存储在:usr/local/redis/data 53 | 54 | 首先我们先配置俩个配置文件,分别为redis6379.conf 和 redis6380.conf 55 | 56 | ![](../images/9823874fa8364612b5a4bdf90b705c4c.jpeg) 57 | 58 | 修改配置文件,主要就是修改端口。为了查看方便在把日志文件和持久化文件的名字都用各自的端口来做标识。 59 | 60 | ![](../images/94dee06ef9b64c118916593c0a47f40e.jpeg) 61 | 62 | 然后分别开启俩个redis服务,一个端口为6379,一个端口为6380。执行命令redis-server redis6380.conf,然后使用redis-cli -p 6380连接,因为redis的默认端口就是6379所以我们启动另外一台redis服务器直接使用redis-server redis6379.conf 然后直接使用redis-cli直接连接就可以。 63 | 64 | ![](../images/cc285906b45845efa6a5193ec1747ce0.jpeg) 65 | 66 | 这个时候我们就成功的配置了俩个redis服务,一台为6380,一台为6379,这里只是为了演示。实际工作中是需要配置在俩台不同的服务器的。 67 | 68 | ![](../images/745a5d13ff6a4dbc89e287f3188ca111.jpeg) 69 | 70 | ### 1. 使用客户端命令行启动 71 | 72 | 我们先得有一个概念,就是在配置主从复制时,所有的操作都是在从节点来操作,也就是slave。 73 | 74 | 那么我们在从节点执行一个命令为 slaveof 127.0.0.1 6379,执行完就代表我们连接上了。 75 | 76 | ![](../images/789513781c64485b9130dc239706fbbf.jpeg) 77 | 78 | 我们先测试一下看是否实现主从复制。在master这台服务器上执行俩个set kaka 123 和 set master 127.0.0.1,然后在slave6380端口是可以成功获取到的,也就说明我们的主从复制就已经配置完成了。但是在实现生产环境可不是就这样完事了,后边会在进一步对主从复制进行优化,直到实现高可用。 79 | 80 | ![](../images/a4d3cb957f3f4b35b280118bb129fe58.jpeg) 81 | 82 | ### 2. 使用配置文件启用 83 | 84 | 在使用配置文件启动主从复制之前呢!先需要把之前使用客户端命令行连接的断开,在从主机执行slaveof no one即可断开主从复制。 85 | 86 | ![](../images/fd9f917bf17e43d5b7d01ed513e05635.jpeg) 87 | 88 | 在哪可以查看从节点已经断开了主节点呢!在主节点的客户端输入命令行info查看 89 | 90 | 这张图是使用从节点使用客户端命令行连接主节点后,在主节点的客户端输入info打印的信息,可以看到有一个slave0的一个信息。 91 | 92 | ![](../images/a2e38eaa9f0541769c7569abffe6da48.jpeg) 93 | 94 | 这个图是在从节点执行完slaveof no one 后,在主节点打印的info,说明从节点已经跟主节点断开连接了。 95 | 96 | ![](../images/dab19075906049dfa6156de45a8bdc1e.jpeg) 97 | 98 | 在根据配置文件启动redis服务,redis-server redis6380.conf 99 | 100 | 当在从节点重新启动后就可以在主节点直接查看到从节点的连接信息。 101 | 102 | ![](../images/d33af09c79c04875b884f51b0c752eeb.jpeg) 103 | 104 | 测试数据,主节点写的东西,从节点还是会自动同步的。 105 | 106 | ![](../images/5a043effdcc54f7bb67e3fa13fcbb2cc.jpeg) 107 | 108 | ### 3. 启动redis服务器时启动 109 | 110 | 这种方式配置也是很简单,在启动redis服务器时直接就启动主从复制,执行命令:redis-server --slaveof host port 即可。 111 | 112 | ### 4. 主从复制启动后的日志信息查看 113 | 114 | 这个是主节点的日志信息 115 | 116 | ![](../images/b68dbf0d64844cf8ab83fca534b04e4a.jpeg) 117 | 118 | 这个是从节点的信息,其中有连接主节点信息,还有RDB快照保存。 119 | 120 | ![](../images/1d905286e19f4a3b960d596b184fa8d5.jpeg) 121 | 122 | ## 五、主从复制工作原理 123 | 124 | ### 1. 主从复制的三个阶段 125 | 126 | 主从复制完整的工作流程分为以下三个阶段。每一段都有自己的内部工作流程,那么我们会对这三个过程进行谈论。 127 | 128 | - 建立连接过程:这个过程就是slave跟master连接的过程 129 | - 数据同步过程:是master给slave同步数据的过程 130 | - 命令传播过程:是反复同步数据 131 | 132 | ![](../images/1727327958931dbc.jpg) 133 | 134 | ### 2. 第一阶段:建立连接过程 135 | 136 | ![](../images/17188139b47a27ca.jpg) 137 | 138 | 上图是一个完整主从复制建立连接工作流程。然后使用简短的话语来描述上边的工作流程。 139 | 140 | 1. 设置master的地址和端口,保存master的信息 141 | 2. 建立socket连接(这个连接做的事情下文会说) 142 | 3. 持续发送ping命令 143 | 4. 身份验证 144 | 5. 发送slave端口信息 145 | 146 | 在建立连接的过程中,从节点会保存master的地址和端口、主节点master保存从节点slave的端口。 147 | 148 | ### 3. 第二阶段:数据同步阶段过程 149 | 150 | ![](../images/17273279684debb7.jpg) 151 | 152 | 这张图是详细描述第一次从节点连接主节点时的数据同步过程。 153 | 当从节点第一次连接主节点时,先会执行一次全量复制这次的全量复制是无法避免的。 154 | 全量复制执行完成后,主节点就会发送复制积压缓冲区的数据,然后从节点就会执行bgrewriteaof恢复数据,这也就是部分复制。 155 | 在这个阶段提到了三个新点,全量复制、部分复制、复制缓冲积压区。会在下文的常见问题里详细说明这几个点。 156 | 157 | ### 4. 第三阶段:命令传播阶段 158 | 159 | 当master数据库被修改后,主从服务器的数据不一致后,此时就会让主从数据同步到一致,这个过程称之为命令传播。 160 | master会将接收到的数据变更命令发送给slave,slave接收命令后执行命令,让主从数据达到一致。 161 | 命令传播阶段的部分复制 162 | 163 | 164 | - 在命令传播阶段出现断网的情况,或者网络抖动时会导致连接断开(connection lost) 165 | - 这个时候主节点master还是会继续往replbackbuffer(复制缓冲积压区)写数据 166 | - 从节点会继续尝试连接主机(connect to master) 167 | - 当从节点把自己的runid和复制偏移量发送给主节点,并且执行pysnc命令同步 168 | - 如果master判断偏移量是在复制缓冲区范围内,就会返回continue命令。并且发送复制缓冲区的数据给从节点。 169 | - 从节点接收数据执行bgrewriteaof,恢复数据 170 | 171 | 172 | ## 六. 详细介绍主从复制原理(全量复制+部分复制) 173 | 174 | ![](../images/1727327968bf0895.jpg) 175 | 176 | 这个过程就是主从复制最齐全的流程讲解。那么下来我们对每一步进程简单的介绍 177 | 178 | 1. 从节点发送指令psync ? 1 psync runid offset 找对应的runid索取数据。但是这里可以考虑一下,当从节点第一次连接的时候根本就不知道主节点的runid 和 offset 。所以第一次发送的指令是psync ? 1意思就是主节点的数据我全要。 179 | 2. 主节点开始执行bgsave生成RDB文件,记录当前的复制偏移量offset 180 | 3. 主节点这个时候会把自己的runid 和 offset 通过 +FULLRESYNC runid offset 指令 通过socket发送RDB文件给从节点。 181 | 4. 从节点接收到+FULLRESYNC 保存主节点的runid和offset 然后清空当前所有数据,通过socket接收RDB文件,开始恢复RDB数据。 182 | 5. 在全量复制后,从节点已经获取到了主节点的runid和offset,开始发送指令 psync runid offset 183 | 6. 主节点接收指令,判断runid是否匹配,判断offset是否在复制缓冲区中。 184 | 7. 主节点判断runid和offset有一个不满足,就会在返回到步骤2继续执行全量复制。这里的runid不匹配只有的可能是从节点重启了这个问题后边会解决,offset(偏移量)不匹配就是复制积压缓冲区溢出了。 如果runid或offset校验通过,从节点的offset和主节点的offset相同时则忽略。 如果runid或offset检验通过,从节点的offset与offset不相同,则会发送 +CONTINUE offset(这个offset为主节点的),通过socket发送复制缓冲区中从节点offset到主节点offset的数据。 185 | 8. 从节点收到+CONTINUE 保存master的offset 通过socket接收到信息后,执行bgrewriteaof,恢复数据。 186 | 187 | **1-4是全量复制 5-8是部分复制** 188 | 189 | 在主节点的第3步下面 主节点在主从复制的期间是一直在接收客户端的数据,主节点的offset是一直变化的。只有有变化就会给每个slave进行发送,这个发送的过程称之为心跳机制 190 | 191 | ## 七. 心跳机制 192 | 193 | 在命令传播阶段是,主节点与从节点之间一直都需要进行信息互换,使用心跳机制进行维护,实现主节点和从节点连接保持在线。 194 | 195 | 196 | - master心跳 197 | - 指令:ping 198 | - 默认10秒进行一次,是由参数repl-ping-slave-period决定的 199 | - 主要做的事情就是判断从节点是否在线 200 | - 可以使用info replication 来查看从节点租后一次连接时间的间隔,lag为0或者为1就是正常状态。 201 | - slave心跳任务 202 | - 指令:replconf ack {offset} 203 | - 每秒执行一次 204 | - 主要做的事情是给主节点发送自己的复制偏移量,从主节点获取到最新的数据变更命令,还做一件事情就是判断主节点是否在线。 205 | 206 | **心跳阶段的注意事项** 主节点为保障数据稳定性,当从节点挂掉的数量或者延迟过高时。将会拒绝所有信息同步。 207 | 208 | 这里有俩个参数可以进行配置调整: 209 | 210 | ```bash 211 | min-slaves-to-write 2 212 | min-slaves-max-lag 8 213 | ``` 214 | 215 | 这俩个参数表示从节点的数量就剩余2个,或者从节点的延迟大于8秒时,主节点就会强制关闭maste功能,停止数据同步。 216 | 217 | 那么主节点是如何知道从节点挂掉的数量和延迟时间呢! 在心跳机制里边slave 会每隔一秒发送perlconf ack 这个指令,这个指令可携带偏移量,也可以携带从节点的延迟时间和从节点的数量。 218 | 219 | ## 八、部分复制的三个核心要素 220 | 221 | ### 1. 服务器的运行id (run id) 222 | 223 | 我们先看一下这个run id是什么,执行info命令即可看到。在上文中我们查看启动日志信息也可以看到。 224 | 225 | ![](../images/172732797248305f.jpg) 226 | 227 | redis在启动时会自动生成一个随机的id(这里需要注意的是每次启动的id都会不一样),是由40个随机的十六进制字符串组成,用来唯一识别一个redis节点。 228 | 在主从复制初次启动时,master会把自己的runid发送给slave,slave会保存master的这个id,我们可以使用info命令查看 229 | 230 | ![](../images/17273279bd947317.jpg) 231 | 232 | 当断线重连时,slave把这个id发送给master,如果slave保存的runid与master现在的runid相同,master会尝试使用部分复制(这块能否复制成功还有一个因素就是偏移量)。如果slave保存的runid与master现在的runid不同,则会直接进行全量复制。 233 | 234 | ### 2. 复制积压缓冲区 235 | 236 | 复制缓冲积压区是一个先进先出的队列,用户存储master收集数据的命令记录。复制缓冲区的默认存储空间是1M。 237 | 可以在配置文件修改repl-backlog-size 1mb来控制缓冲区大小,这个比例可以根据自己的服务器内存来修改,咔咔这边是预留出了30%左右。 238 | 239 | **复制缓冲区到底存储的是什么?** 240 | 241 | 当执行一个命令为set name kaka时,我们可以查看持久化文件查看 242 | 243 | ![](../images/172732798545411e.jpg) 244 | 245 | 那么复制积压缓冲区就是存储的aof持久化的数据,并且以字节分开,并且每个字节都有自己的偏移量。这个偏移量也就是复制偏移量(offset) 246 | 247 | ![](../images/172732798af59f73.jpg) 248 | 249 | 那为什么会说复制缓冲积压区有可能会导致全量复制呢 250 | 在命令传播阶段,主节点会把收集的数据存储到复制缓冲区中,然后在发送给从节点。就是这里出现了问题,当主节点数据量在一瞬间特别大的时候,超出了复制缓冲区的内存,就会有一部分数据会被挤出去,从而导致主节点和从节点的数据不一致。从而进行全量复制。如果这个缓冲区大小设置不合理那么很大可能会造成死循环,从节点就会一直全量复制,清空数据,全量复制。 251 | 252 | ### 3. 复制偏移量(offset) 253 | 254 | ![](../images/17273279906f511c.jpg) 255 | 256 | 主节点复制偏移量是给从节点发送一次记录一次,从节点是接收一次记录一次。 257 | 用于同步信息,对比主节点和从节点的差异,当slave断联时恢复数据使用。 258 | 这个值也就是来自己于复制缓冲积压区里边的那个偏移量。 259 | 260 | ## 九. 主从复制常见的问题 261 | 262 | ### 1. 主节点重启问题(内部优化) 263 | 264 | 当主节点重启后,runid的值将发生变化,会导致所有的从节点进行全量复制。 265 | 266 | 这个问题我们无需考虑,知道系统是怎么优化的即可。 267 | 268 | 在建立完主从复制后主节点会创建master-replid变量,这个生成的策略跟runid一样,长度是41位,runid长度是40位,然后发送给从节点。 269 | 270 | 在主节点执行shutdown save命令时,进行了一次RDB持久化会把runid 和 offset保存到RDB文件中。可以使用命令redis-check-rdb查看该信息。 271 | 272 | ![](../images/17273279a6e90503.jpg) 273 | 274 | 主节点重启后加载RDB文件,将文件中的repl-id 和repl-offset加载到内存中。纵使让所有从节点认为还是之前的主节点。 275 | 276 | ### 2. 从节点网络中断偏移量越界导致全量复制 277 | 278 | 由于网络环境不佳,从节点网络中断。复制积压缓冲区内存过小导致数据溢出,伴随着从节点偏移量越界,导致全量复制。有可能会导致反复的全量复制。 279 | 解决方案:修改复制积压缓冲区的大小:repl-backlog-size 280 | 设置建议:测试主节点连接从节点的时间,获取主节点每秒平均产生的命令总量write_size_per_second 281 | 复制缓冲区空间设置 = 2 * 主从连接时间 * 主节点每秒产生的数据总量 282 | 283 | ### 3. 频繁的网路中断 284 | 285 | 由于主节点的cpu占用过高,或者从节点频繁连接。出现这种情况造成的结果就是主节点各种资源被严重占用,其中包括但不限于缓冲区,宽带,连接等。 286 | 为什么会出现主节点资源被严重占用? 287 | 在心跳机制中,从节点每秒会发送一个指令replconf ack指令到主节点。 288 | 从节点执行了慢查询,占用大量的cpu 289 | 主节点每秒调用复制定时函数replicationCron,然后从节点长时间没有相应。 290 | 291 | **解决方案:** 292 | 293 | - 设置从节点超时释放 294 | - 设置参数:repl-timeout 295 | - 这个参数默认为60秒。超过60秒,释放slave。 296 | 297 | ### 4. 数据不一致问题 298 | 299 | 由于网络因素,多个从节点的数据会不一致。这个因素是没有办法避免的。 300 | 301 | **关于这个问题给出俩个解决方案:** 302 | 303 | - 第一个数据需要高度一致配置一台redis服务器,读写都用一台服务器,这种方式仅限于少量数据,并且数据需高度一直。 304 | - 第二个监控主从节点的偏移量,如果从节点的延迟过大,暂时屏蔽客户端对该从节点的访问。设置参数为slave-serve-stale-data yes|no。 这个参数一但设置就只能响应info slaveof等少数命令。 305 | 306 | **5. 从节点故障** 307 | 308 | 这个问题直接在客户端维护一个可用节点列表,当从节点故障时,切换到其他节点进行工作,这个问题在后边集群会说到。 309 | 310 | ## 十. 总结 311 | 312 | 本文主要讲解了什么是主从复制、主从复制工作的三大阶段以及工作流程、部分复制的三大核心。命令传播阶段的心跳机制。最后说明了主从复制常见问题。 313 | 314 | 315 | >作者:原来是咔咔 316 | >链接:https://juejin.im/post/5ed5ccb66fb9a047df7ca9a4 317 | >来源:掘金 318 | >著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | -------------------------------------------------------------------------------- /redis/redis-policy.md: -------------------------------------------------------------------------------- 1 | # Redis中内存淘汰算法实现 2 | 3 | Redis的`maxmemory`支持的内存淘汰机制使得其成为一种有效的缓存方案,成为memcached的有效替代方案。 4 | 5 | 当内存达到`maxmemory`后,Redis会按照`maxmemory-policy`启动淘汰策略。 6 | 7 | Redis 3.0中已有淘汰机制: 8 | 9 | - noeviction 10 | - allkeys-lru 11 | - volatile-lru 12 | - allkeys-random 13 | - volatile-random 14 | - volatile-ttl 15 | 16 | | maxmemory-policy | 含义 |特性 | 17 | | ----- | ----- | ----- | 18 | noeviction |不淘汰 |内存超限后写命令会返回错误(如OOM, del命令除外) 19 | allkeys-lru |所有key的LRU机制 在|所有key中按照最近最少使用LRU原则剔除key,释放空间 20 | volatile-lru |易失key的LRU |仅以设置过期时间key范围内的LRU(如均为设置过期时间,则不会淘汰) 21 | allkeys-random |所有key随机淘汰| 一视同仁,随机 22 | volatile-random |易失Key的随机 |仅设置过期时间key范围内的随机 23 | volatile-ttl |易失key的TTL淘汰| 按最小TTL的key优先淘汰 24 | 25 | 其中LRU(less recently used)经典淘汰算法在Redis实现中有一定优化设计,来保证内存占用与实际效果的平衡,这也体现了工程应用是空间与时间的平衡性。 26 | 27 | > PS:值得注意的,在主从复制模式Replication下,从节点达到maxmemory时不会有任何异常日志信息,但现象为增量数据无法同步至从节点。 28 | 29 | ## Redis 3.0中近似LRU算法 30 | 31 | Redis中LRU是近似LRU实现,并不能取出理想LRU理论中最佳淘汰Key,而是通过从小部分采样后的样本中淘汰局部LRU键。 32 | 33 | Redis 3.0中近似LRU算法通过增加待淘汰元素池的方式进一步优化,最终实现与精确LRU非常接近的表现。 34 | 35 | > 精确LRU会占用较大内存记录历史状态,而近似LRU则用较小内存支出实现近似效果。 36 | 37 | 以下是理论LRU和近似LRU的效果对比: 38 | 39 | ![](../images/lru_comparison.png) 40 | 41 | - 按时间顺序接入不同键,此时最早写入也就是最佳淘汰键 42 | - 浅灰色区域:被淘汰的键 43 | - 灰色区域:未被淘汰的键 44 | - 绿色区域:新增写入的键 45 | 46 | 总结图中展示规律, 47 | 48 | - 图1Theoretical LRU符合预期:最早写入键逐步被淘汰 49 | - 图2Approx LRU Redis 3.0 10 samples:Redis 3.0中近似LRU算法(采样值为10) 50 | - 图3Approx LRU Redis 2.8 5 samples:Redis 2.8中近似LRU算法(采样值为5) 51 | - 图4Approx LRU Redis 3.0 5 samples:Redis 3.0中近似LRU算法(采样值为5) 52 | 53 | 结论: 54 | 55 | - 通过图4和图3对比:得出相同采样值下,3.0比2.8的LRU淘汰机制更接近理论LRU 56 | - 通过图4和图2对比:得出增加采样值,在3.0中将进一步改善LRU淘汰效果逼近理论LRU 57 | - 对比图2和图1:在3.0中采样值为10时,效果非常接近理论LRU 58 | 59 | 采样值设置通过maxmemory-samples指定,可通过CONFIG SET maxmemory-samples 动态设置,也可启动配置中指定maxmemory-samples 60 | 61 | 源码解析 62 | 63 | ```cgo 64 | int freeMemoryIfNeeded(void){ 65 | while (mem_freed < mem_tofree) { 66 | if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION) 67 | return REDIS_ERR; /* We need to free memory, but policy forbids. */ 68 | 69 | if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU || 70 | server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM) 71 | {......} 72 | /* volatile-random and allkeys-random policy */ 73 | if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM || 74 | server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM) 75 | {......} 76 | /* volatile-lru and allkeys-lru policy */ 77 | else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU || 78 | server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) 79 | { 80 | // 淘汰池函数 81 | evictionPoolPopulate(dict, db->dict, db->eviction_pool); 82 | while(bestkey == NULL) { 83 | evictionPoolPopulate(dict, db->dict, db->eviction_pool); 84 | // 从后向前逐一淘汰 85 | for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) { 86 | if (pool[k].key == NULL) continue; 87 | de = dictFind(dict,pool[k].key); // 定位目标 88 | 89 | /* Remove the entry from the pool. */ 90 | sdsfree(pool[k].key); 91 | /* Shift all elements on its right to left. */ 92 | memmove(pool+k,pool+k+1, 93 | sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1)); 94 | /* Clear the element on the right which is empty 95 | * since we shifted one position to the left. */ 96 | pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL; 97 | pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0; 98 | 99 | /* If the key exists, is our pick. Otherwise it is 100 | * a ghost and we need to try the next element. */ 101 | if (de) { 102 | bestkey = dictGetKey(de); // 确定删除键 103 | break; 104 | } else { 105 | /* Ghost... */ 106 | continue; 107 | } 108 | } 109 | } 110 | } 111 | /* volatile-ttl */ 112 | else if (server.maxmemory_policy == EDIS_MAXMEMORY_VOLATILE_TTL) {......} 113 | 114 | // 最终选定待删除键bestkey 115 | if (bestkey) { 116 | long long delta; 117 | robj *keyobj = createStringObject(bestkey,sdslenbestkey)); // 目标对象 118 | propagateExpire(db,keyobj); 119 | latencyStartMonitor(eviction_latency); // 延迟监控开始 120 | dbDelete(db,keyobj); // 从db删除对象 121 | latencyEndMonitor(eviction_latency);// 延迟监控结束 122 | latencyAddSampleIfNeeded("eviction-del",iction_latency); // 延迟采样 123 | latencyRemoveNestedEvent(latency,eviction_latency); 124 | delta -= (long long) zmalloc_used_memory(); 125 | mem_freed += delta; // 释放内存计数 126 | server.stat_evictedkeys++; // 淘汰key计数,info中可见 127 | notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted", keyobj, db->id); // 事件通知 128 | decrRefCount(keyobj); // 引用计数更新 129 | keys_freed++; 130 | // 避免删除较多键导致的主从延迟,在循环内同步 131 | if (slaves) flushSlavesOutputBuffers(); 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | 138 | ## Redis 4.0中新的LFU算法 139 | 140 | 从Redis4.0开始,新增LFU淘汰机制,提供更好缓存命中率。LFU(Least Frequently Used)通过记录键使用频率来定位最可能淘汰的键。 141 | 142 | 对比LRU与LFU的差别: 143 | 144 | - 在LRU中,某个键很少被访问,但在刚刚被访问后其被淘汰概率很低,从而出现这类异常持续存在的缓存;相对的,其他可能被访问的键会被淘汰 145 | - 而LFU中,按访问频次淘汰最少被访问的键 146 | 147 | Redis 4.0中新增两种LFU淘汰机制: 148 | 149 | - volatile-lfu:设置过期时间的键按LFU淘汰 150 | - allkeys-lfu:所有键按LFU淘汰 151 | 152 | LFU使用Morris counters计数器占用少量位数来评估每个对象的访问频率,并随时间更新计数器。此机制实现与近似LRU中采样类似。但与LRU不同,LFU提供明确参数来指定计数更新频率。 153 | 154 | - lfu-log-factor:0-255之间,饱和因子,值越小代表饱和速度越快 155 | - lfu-decay-time:衰减周期,单位分钟,计数器衰减的分钟数 156 | 157 | 这两个因子形成一种平衡,通过少量访问 VS 多次访问 的评价标准最终形成对键重要性的评判。 158 | 159 | > 原文: 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/q001.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | func main() { 10 | 11 | letter, number := make(chan bool), make(chan bool) 12 | wait := sync.WaitGroup{} 13 | 14 | go func() { 15 | i := 1 16 | for { 17 | select { 18 | case <-number: 19 | fmt.Print(i) 20 | i++ 21 | fmt.Print(i) 22 | i++ 23 | letter <- true 24 | break 25 | default: 26 | break 27 | } 28 | } 29 | }() 30 | wait.Add(1) 31 | go func(wait *sync.WaitGroup) { 32 | str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 33 | 34 | i := 0 35 | for { 36 | select { 37 | case <-letter: 38 | if i >= strings.Count(str, "")-1 { 39 | wait.Done() 40 | return 41 | } 42 | 43 | fmt.Print(str[i : i+1]) 44 | i++ 45 | if i >= strings.Count(str, "") { 46 | i = 0 47 | } 48 | fmt.Print(str[i : i+1]) 49 | i++ 50 | number <- true 51 | break 52 | default: 53 | break 54 | } 55 | 56 | } 57 | }(&wait) 58 | number <- true 59 | wait.Wait() 60 | } 61 | -------------------------------------------------------------------------------- /src/q002.go: -------------------------------------------------------------------------------- 1 | //判断字符串中字符是否全都不同 2 | package main 3 | 4 | import ( 5 | "strings" 6 | "fmt" 7 | ) 8 | 9 | func isUniqueString(s string) bool { 10 | if strings.Count(s,"") > 3000{ 11 | return false 12 | } 13 | for _,v := range s { 14 | if v > 127 { 15 | return false 16 | } 17 | if strings.Count(s,string(v)) > 1 { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | 24 | func isUniqueString2(s string) bool { 25 | if strings.Count(s,"") > 3000{ 26 | return false 27 | } 28 | for _,v := range s { 29 | if v > 127 { 30 | return false 31 | } 32 | if strings.Index(s,string(v)) != strings.LastIndex(s,string(v)) { 33 | return false 34 | } 35 | } 36 | return true 37 | } 38 | func main() { 39 | str := "abfgasdgfdgfdfdfds" 40 | 41 | fmt.Println(isUniqueString2(str)) 42 | 43 | str = "abcdefG" 44 | fmt.Println(isUniqueString2(str)) 45 | } -------------------------------------------------------------------------------- /src/q003.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func reverString(s string) (string, bool) { 8 | str := []rune(s) 9 | l := len(str) 10 | if l > 5000 { 11 | return string(str), false 12 | } 13 | for i := 0; i < l/2; i++ { 14 | str[i], str[l-1-i] = str[l-1-i], str[i] 15 | } 16 | return string(str), true 17 | } 18 | 19 | func main() { 20 | s1 := "This is golang" 21 | fmt.Println(reverString(s1)) 22 | 23 | s2 := "gnalog si sihT" 24 | fmt.Println(reverString(s2)) 25 | } -------------------------------------------------------------------------------- /src/q004.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "fmt" 6 | ) 7 | 8 | func isRegroup(s1,s2 string) bool { 9 | sl1 := len([]rune(s1)) 10 | sl2 := len([]rune(s2)) 11 | 12 | if sl1 > 5000 || sl2 > 5000 || sl1 != sl2{ 13 | return false 14 | } 15 | 16 | for _,v := range s1 { 17 | if strings.Count(s1,string(v)) != strings.Count(s2,string(v)) { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | 24 | func main() { 25 | s1 := "This is golang" 26 | s2 := "gnalog si sihT" 27 | fmt.Println(isRegroup(s1, s2)) 28 | 29 | s3 := "Here you are" 30 | s4 := "Are you here" 31 | fmt.Println(isRegroup(s3, s4)) 32 | 33 | s5 := "This is golang1.1" 34 | s6 := "This is golang1" 35 | fmt.Println(isRegroup(s5, s6)) 36 | } 37 | -------------------------------------------------------------------------------- /src/q005.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | func replaceBlank(s string) (string, bool) { 10 | if len([]rune(s)) > 1000 { 11 | return s, false 12 | } 13 | for _, v := range s { 14 | if string(v) != " " && unicode.IsLetter(v) == false { 15 | return s, false 16 | } 17 | } 18 | return strings.Replace(s, " ", "%20", -1), true 19 | } 20 | 21 | func main() { 22 | s1 := "Hello World" 23 | fmt.Println(replaceBlank(s1)) 24 | 25 | s2 := "Hello,World" 26 | fmt.Println(replaceBlank(s2)) 27 | } -------------------------------------------------------------------------------- /src/q006.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func run(s string) (x, y int) { 10 | cmdList := resolveCmd(s) 11 | 12 | face := "Y" 13 | for _,c := range cmdList { 14 | if c == "L" { 15 | if face == "Y" { 16 | face = "-X" 17 | }else if face == "-Y"{ 18 | face = "X" 19 | }else if face == "X" { 20 | face = "Y" 21 | }else { 22 | face = "-Y" 23 | } 24 | }else if c == "R" { 25 | if face == "Y" { 26 | face = "X" 27 | }else if face == "-Y"{ 28 | face = "-X" 29 | }else if face == "X" { 30 | face = "-Y" 31 | }else { 32 | face = "Y" 33 | } 34 | }else if c == "F" { 35 | if face == "Y" { 36 | y += 1 37 | }else if face == "-Y" { 38 | y -= 1 39 | }else if face == "X" { 40 | x+=1 41 | }else { 42 | x-=1 43 | } 44 | }else if c == "B" { 45 | if face == "Y" { 46 | y-=1 47 | }else if face == "-Y" { 48 | y+=1 49 | }else if face == "X" { 50 | x+=1 51 | }else { 52 | x-=1 53 | } 54 | } 55 | } 56 | return 57 | } 58 | 59 | func resolveCmd(s string) ([]string){ 60 | 61 | cmdList := make([]string,0) 62 | repeatCount := 0 63 | isStart := false 64 | tempCmd := "" 65 | 66 | for _,v := range s { 67 | ns := string(v) 68 | //如果是字符串,则标识下一步是重复步骤 69 | if ns >= "0" && ns <= "9" { 70 | t,_ := strconv.Atoi(ns); 71 | repeatCount = t 72 | }else if ns == "(" { 73 | isStart = true 74 | }else if ns == ")" { 75 | c := strings.Repeat(tempCmd,repeatCount) 76 | 77 | tempList := make([]string,strings.Count(c,"")) 78 | 79 | for i,v1 := range c { 80 | tempList[i] = string(v1) 81 | } 82 | 83 | //当解析结束时,重复命令并保存到列表中 84 | cmdList = append(cmdList,tempList...) 85 | isStart = false 86 | repeatCount = 0 87 | tempCmd = "" 88 | }else if isStart{ 89 | tempCmd += ns 90 | }else{ 91 | cmdList = append(cmdList,ns) 92 | } 93 | } 94 | return cmdList 95 | } 96 | 97 | func main() { 98 | s := "R2(LF)" 99 | 100 | fmt.Println(run(s)) 101 | } 102 | -------------------------------------------------------------------------------- /src/q007.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type People interface { 8 | Show() 9 | } 10 | 11 | type Student struct{} 12 | 13 | func (stu *Student) Show() { 14 | 15 | } 16 | 17 | func live() People { 18 | var stu *Student 19 | 20 | return stu 21 | } 22 | 23 | func main() { 24 | if live() == nil { 25 | fmt.Println("AAAAAAA") 26 | } else { 27 | fmt.Println("BBBBBBB") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/q009.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | ) 8 | 9 | func main() { 10 | out := make(chan int) 11 | wg := sync.WaitGroup{} 12 | wg.Add(2) 13 | go func() { 14 | defer wg.Done() 15 | for i := 0; i < 5; i++ { 16 | out <- rand.Intn(5) 17 | } 18 | close(out) 19 | }() 20 | go func() { 21 | defer wg.Done() 22 | for i := range out { 23 | fmt.Println(i) 24 | } 25 | }() 26 | wg.Wait() 27 | } 28 | -------------------------------------------------------------------------------- /src/q010.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Time : 2021/1/29 上午9:31 3 | @Author : sunyujia 4 | @File : q010 5 | @Software: GoLand 6 | */ 7 | package main 8 | 9 | import ( 10 | "log" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | type sp interface { 16 | Out(key string, val interface{}) //存入key /val,如果该key读取的goroutine挂起,则唤醒。此方法不会阻塞,时刻都可以立即执行并返回 17 | Rd(key string, timeout time.Duration) interface{} //读取一个key,如果key不存在阻塞,等待key存在或者超时 18 | } 19 | 20 | type Map struct { 21 | c map[string]*entry 22 | rmx *sync.RWMutex 23 | } 24 | type entry struct { 25 | ch chan struct{} 26 | value interface{} 27 | isExist bool 28 | } 29 | 30 | func (m *Map) Out(key string, val interface{}) { 31 | m.rmx.Lock() 32 | defer m.rmx.Unlock() 33 | item, ok := m.c[key] 34 | if !ok { 35 | m.c[key] = &entry{ 36 | value: val, 37 | isExist: true, 38 | } 39 | return 40 | } 41 | item.value = val 42 | if !item.isExist { 43 | if item.ch != nil { 44 | close(item.ch) 45 | item.ch = nil 46 | } 47 | } 48 | return 49 | } 50 | 51 | func (m *Map) Rd(key string, timeout time.Duration) interface{} { 52 | m.rmx.RLock() 53 | if e, ok := m.c[key]; ok && e.isExist { 54 | m.rmx.RUnlock() 55 | return e.value 56 | } else if !ok { 57 | m.rmx.RUnlock() 58 | m.rmx.Lock() 59 | e = &entry{ch: make(chan struct{}), isExist: false} 60 | m.c[key] = e 61 | m.rmx.Unlock() 62 | log.Println("协程阻塞 -> ", key) 63 | select { 64 | case <-e.ch: 65 | return e.value 66 | case <-time.After(timeout): 67 | log.Println("协程超时 -> ", key) 68 | return nil 69 | } 70 | } else { 71 | m.rmx.RUnlock() 72 | log.Println("协程阻塞 -> ", key) 73 | select { 74 | case <-e.ch: 75 | return e.value 76 | case <-time.After(timeout): 77 | log.Println("协程超时 -> ", key) 78 | return nil 79 | } 80 | } 81 | } 82 | 83 | func main() { 84 | mapval := Map{ 85 | c: make(map[string]*entry), 86 | rmx: &sync.RWMutex{}, 87 | } 88 | 89 | for i := 0; i < 10; i++ { 90 | go func() { 91 | val := mapval.Rd("key", time.Second*6) 92 | log.Println("读取值为->", val) 93 | }() 94 | } 95 | 96 | time.Sleep(time.Second * 3) 97 | for i := 0; i < 10; i++ { 98 | go func(val int) { 99 | mapval.Out("key", val) 100 | }(i) 101 | } 102 | 103 | time.Sleep(time.Second * 30) 104 | } 105 | func init() { 106 | log.SetFlags(log.LstdFlags | log.Lshortfile) 107 | } 108 | -------------------------------------------------------------------------------- /src/q011.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | type Ban struct { 12 | visitIPs map[string]time.Time 13 | lock sync.Mutex 14 | } 15 | 16 | func NewBan(ctx context.Context) *Ban { 17 | o := &Ban{visitIPs: make(map[string]time.Time)} 18 | go func() { 19 | timer := time.NewTimer(time.Minute * 1) 20 | for { 21 | select { 22 | case <-timer.C: 23 | o.lock.Lock() 24 | for k, v := range o.visitIPs { 25 | if time.Now().Sub(v) >= time.Minute*1 { 26 | delete(o.visitIPs, k) 27 | } 28 | } 29 | o.lock.Unlock() 30 | timer.Reset(time.Minute * 1) 31 | case <-ctx.Done(): 32 | return 33 | } 34 | } 35 | }() 36 | return o 37 | } 38 | func (o *Ban) visit(ip string) bool { 39 | o.lock.Lock() 40 | defer o.lock.Unlock() 41 | if _, ok := o.visitIPs[ip]; ok { 42 | return true 43 | } 44 | o.visitIPs[ip] = time.Now() 45 | return false 46 | } 47 | func main() { 48 | success := int64(0) 49 | ctx, cancel := context.WithCancel(context.Background()) 50 | defer cancel() 51 | 52 | ban := NewBan(ctx) 53 | 54 | wait := &sync.WaitGroup{} 55 | 56 | wait.Add(1000 * 100) 57 | for i := 0; i < 1000; i++ { 58 | for j := 0; j < 100; j++ { 59 | go func(j int) { 60 | defer wait.Done() 61 | ip := fmt.Sprintf("192.168.1.%d", j) 62 | if !ban.visit(ip) { 63 | atomic.AddInt64(&success, 1) 64 | } 65 | }(j) 66 | } 67 | 68 | } 69 | wait.Wait() 70 | 71 | fmt.Println("success:", success) 72 | } 73 | -------------------------------------------------------------------------------- /src/q012.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func main() { 9 | go func() { 10 | // 1 在这里需要你写算法 11 | // 2 要求每秒钟调用一次proc函数 12 | // 3 要求程序不能退出 13 | 14 | t := time.NewTicker(time.Second * 1) 15 | for { 16 | select { 17 | case <-t.C: 18 | go func() { 19 | defer func() { 20 | if err := recover(); err != nil { 21 | fmt.Println(err) 22 | } 23 | }() 24 | proc() 25 | }() 26 | } 27 | } 28 | }() 29 | 30 | select {} 31 | } 32 | 33 | func proc() { 34 | panic("ok") 35 | } 36 | -------------------------------------------------------------------------------- /src/q013.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | wg := sync.WaitGroup{} 11 | c := make(chan struct{}) 12 | for i := 0; i < 10; i++ { 13 | wg.Add(1) 14 | go func(num int, close <-chan struct{}) { 15 | defer wg.Done() 16 | <-close 17 | fmt.Println(num) 18 | }(i, c) 19 | } 20 | 21 | if WaitTimeout(&wg, time.Second*5) { 22 | close(c) 23 | fmt.Println("timeout exit") 24 | } 25 | time.Sleep(time.Second * 10) 26 | } 27 | 28 | func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { 29 | // 要求手写代码 30 | // 要求sync.WaitGroup支持timeout功能 31 | // 如果timeout到了超时时间返回true 32 | // 如果WaitGroup自然结束返回false 33 | ch := make(chan bool, 1) 34 | 35 | go time.AfterFunc(timeout, func() { 36 | ch <- true 37 | }) 38 | 39 | go func() { 40 | wg.Wait() 41 | ch <- false 42 | }() 43 | return <-ch 44 | } 45 | -------------------------------------------------------------------------------- /src/q014.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type query func(string) string 9 | 10 | func exec(name string, vs ...query) string { 11 | ch := make(chan string) 12 | ctx, cancel := context.WithCancel(context.Background()) 13 | 14 | fn := func(i int) { 15 | select { 16 | case ch <- vs[i](name): 17 | case <-ctx.Done(): 18 | return 19 | } 20 | } 21 | for i := range vs { 22 | go fn(i) 23 | } 24 | defer cancel() 25 | return <-ch 26 | } 27 | 28 | func main() { 29 | ret := exec("111", func(n string) string { 30 | return n + "func1" 31 | }, func(n string) string { 32 | return n + "func2" 33 | }, func(n string) string { 34 | return n + "func3" 35 | }, func(n string) string { 36 | return n + "func4" 37 | }) 38 | fmt.Println(ret) 39 | } 40 | -------------------------------------------------------------------------------- /src/q017.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | timer := time.NewTimer(time.Second * 5) 13 | data := []int{1, 2, 3, 10, 999, 8, 345, 7, 98, 33, 66, 77, 88, 68, 96} 14 | dataLen := len(data) 15 | size := 3 16 | target := 345 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | resultChan := make(chan bool) 19 | wg := &sync.WaitGroup{} 20 | finishChan := make(chan struct{}) 21 | for i := 0; i < dataLen; i += size { 22 | end := i + size 23 | if end >= dataLen { 24 | end = dataLen - 1 25 | } 26 | wg.Add(1) 27 | go func() { 28 | defer wg.Done() 29 | SearchTarget(ctx, data[i:end], target, resultChan) 30 | }() 31 | } 32 | go func() { 33 | wg.Wait() 34 | finishChan <- struct{}{} 35 | }() 36 | select { 37 | case <-timer.C: 38 | fmt.Fprintln(os.Stderr, "Timeout! Not Found") 39 | cancel() 40 | case <-resultChan: 41 | fmt.Fprintf(os.Stdout, "Found it!\n") 42 | cancel() 43 | case <- finishChan: 44 | fmt.Printf("[%d] not found in slice\n", target) 45 | } 46 | 47 | time.Sleep(time.Second * 2) 48 | } 49 | 50 | func SearchTarget(ctx context.Context, data []int, target int, resultChan chan bool) { 51 | for _, v := range data { 52 | select { 53 | case <-ctx.Done(): 54 | fmt.Fprintf(os.Stdout, "Task cancelded! \n") 55 | return 56 | default: 57 | } 58 | // 模拟一个耗时查找,这里只是比对值,真实开发中可以是其他操作 59 | fmt.Fprintf(os.Stdout, "v: %d \n", v) 60 | time.Sleep(time.Millisecond * 1500) 61 | if target == v { 62 | resultChan <- true 63 | return 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/q018.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("以下是数值的chan") 7 | ci := make(chan int, 3) 8 | ci <- 1 9 | close(ci) 10 | num, ok := <-ci 11 | fmt.Printf("读chan的协程结束,num=%v, ok=%v\n", num, ok) 12 | num1, ok1 := <-ci 13 | fmt.Printf("再读chan的协程结束,num=%v, ok=%v\n", num1, ok1) 14 | num2, ok2 := <-ci 15 | fmt.Printf("再再读chan的协程结束,num=%v, ok=%v\n", num2, ok2) 16 | 17 | fmt.Println("以下是字符串chan") 18 | cs := make(chan string, 3) 19 | cs <- "aaa" 20 | close(cs) 21 | str, ok := <-cs 22 | fmt.Printf("读chan的协程结束,str=%v, ok=%v\n", str, ok) 23 | str1, ok1 := <-cs 24 | fmt.Printf("再读chan的协程结束,str=%v, ok=%v\n", str1, ok1) 25 | str2, ok2 := <-cs 26 | fmt.Printf("再再读chan的协程结束,str=%v, ok=%v\n", str2, ok2) 27 | 28 | fmt.Println("以下是结构体chan") 29 | type MyStruct struct { 30 | Name string 31 | } 32 | cstruct := make(chan MyStruct, 3) 33 | cstruct <- MyStruct{Name: "haha"} 34 | close(cstruct) 35 | stru, ok := <-cstruct 36 | fmt.Printf("读chan的协程结束,stru=%v, ok=%v\n", stru, ok) 37 | stru1, ok1 := <-cs 38 | fmt.Printf("再读chan的协程结束,stru=%v, ok=%v\n", stru1, ok1) 39 | stru2, ok2 := <-cs 40 | fmt.Printf("再再读chan的协程结束,stru=%v, ok=%v\n", stru2, ok2) 41 | } 42 | -------------------------------------------------------------------------------- /src/q15_2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var mu sync.RWMutex 10 | var count int 11 | 12 | func main() { 13 | go A() 14 | time.Sleep(2 * time.Second) 15 | mu.Lock() 16 | defer mu.Unlock() 17 | count++ 18 | fmt.Println(count) 19 | } 20 | func A() { 21 | mu.RLock() 22 | defer mu.RUnlock() 23 | B() 24 | } 25 | func B() { 26 | time.Sleep(5 * time.Second) 27 | C() 28 | } 29 | func C() { 30 | mu.RLock() 31 | defer mu.RUnlock() 32 | } 33 | --------------------------------------------------------------------------------