├── go.mod ├── cover.jpeg ├── exercise_book.md ├── data_structure ├── stack │ └── main.go └── linked_list │ ├── main.go │ └── example.go ├── cmd └── carol.go ├── README.md └── carol.go /go.mod: -------------------------------------------------------------------------------- 1 | module weizicoding.com/carol 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/go-carol/HEAD/cover.jpeg -------------------------------------------------------------------------------- /exercise_book.md: -------------------------------------------------------------------------------- 1 | * 2023-08-30: data_structure/linked_list 2 | * 2023-08-28: data_structure/linked_list, data_structure/stack 3 | -------------------------------------------------------------------------------- /data_structure/stack/main.go: -------------------------------------------------------------------------------- 1 | // https://www.hello-algo.com/chapter_stack_and_queue/stack/ 2 | // 3 | // Level: medium 4 | // Topics: 数据结构 5 | package main 6 | -------------------------------------------------------------------------------- /data_structure/linked_list/main.go: -------------------------------------------------------------------------------- 1 | // https://www.hello-algo.com/chapter_array_and_linkedlist/linked_list/ 2 | // 3 | // Level: medium 4 | // Topics: 数据结构 5 | package main 6 | -------------------------------------------------------------------------------- /cmd/carol.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "weizicoding.com/carol" 6 | ) 7 | 8 | var showLevel = flag.String("l", "", "显示练习难度, easy; medium; hard") 9 | var sortByColumn = flag.Int("s", 2, "排序依据, 1:名称; 2:上一次完成时间; 3:完成次数") 10 | var showLastDoneDaysAgo = flag.Int("d", -1, "指定上一次完成时间范围") 11 | 12 | func main() { 13 | flag.Parse() 14 | 15 | practices, err := carol.Get() 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | // 解析控制台打印格式 21 | carol.Print(practices, *showLastDoneDaysAgo, *sortByColumn, *showLevel) 22 | } 23 | -------------------------------------------------------------------------------- /data_structure/linked_list/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type ListNode struct { 4 | Val int // 节点值 5 | Next *ListNode // 指向下一个节点的指针 6 | } 7 | 8 | func NewListNode(val int) *ListNode { 9 | return &ListNode{ 10 | Val: val, 11 | Next: nil, 12 | } 13 | } 14 | 15 | func main() { 16 | // 初始化链表 17 | n0 := NewListNode(1) 18 | n1 := NewListNode(3) 19 | n2 := NewListNode(2) 20 | n3 := NewListNode(5) 21 | n4 := NewListNode(4) 22 | 23 | // 构建引用指向 24 | n0.Next = n1 25 | n1.Next = n2 26 | n2.Next = n3 27 | n3.Next = n4 28 | } 29 | 30 | /* 在链表的节点 n0 之后插入节点 P */ 31 | func insertNode(n0 *ListNode, P *ListNode) { 32 | n1 := n0.Next 33 | P.Next = n1 34 | n0.Next = P 35 | } 36 | 37 | /* 在链表中删除节点 P */ 38 | func removeNode(n0 *ListNode) { 39 | if n0.Next == nil { 40 | return 41 | } 42 | // n0 -> P -> n1 43 | P := n0.Next 44 | n1 := P.Next 45 | n0.Next = n1 46 | } 47 | 48 | /* 访问链表中索引为 index 的节点 */ 49 | func access(head *ListNode, index int) *ListNode { 50 | for i := 0; i < index; i++ { 51 | if head == nil { 52 | return nil 53 | } 54 | head = head.Next 55 | } 56 | return head 57 | } 58 | 59 | /* 在链表中查找值为 target 的首个节点 */ 60 | func findNode(head *ListNode, target int) int { 61 | var index int 62 | for head != nil { 63 | if head.Val == target { 64 | return index 65 | } 66 | head = head.Next 67 | index++ 68 | } 69 | return -1 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cover.jpeg](cover.jpeg) 2 | 3 | # Carol 4 | 5 | > 心理学家 Carol Dweck 做过一个实验,她找了一些十岁的孩子,随机分成两组,让他们做道题。 6 | > 7 | > 之后,对第一组那些完成题目的孩子说:你真聪明。对第二组那些做得不错的孩子说:你真努力,你很认真。 8 | > 9 | > 你应该感受不到其中的差别,没关系,我们接着说第二部分研究。接下来,她让两组孩子从两道题目中选一道去做,一道“很简单”,另一道“非常之困难”,“但是他们能够从中学到很多”。 10 | > 11 | > 巨大的差别出现了:被夸聪明的第一组孩子,有五成选了简单的题目;被夸努力的第二组孩子,有九成选了非常之困难的题目。 12 | > 13 | > 继续看第三部分的研究。 14 | > 15 | > 她继续让两组孩子做一道非常难的题目,基本上可以说无法解答。“聪明”组几乎没坚持多久,非常沮丧,很快就放弃了;而“努力认真”组,坚持了很长时间,而且很享受这一过程,虽然最后也没能解开这道题,但他们很少有负面情绪。 16 | > 17 | > 发现问题了吗?被夸奖“聪明”,只要做事的时候遇到困难,你就非常容易陷入自我怀疑和沮丧,立刻放弃;而被夸奖“努力认真”呢? 18 | > 19 | > 重视努力让孩子拥有一个 TA 自己能掌控的变量,这会让他们认为自己能掌控自己的成功。 20 | > 21 | > 而重视天赋,会让我们秉持固定型思维,认为聪明与否是无法改变的。我们不敢去尝试有难度的事情,因为如果失败了,就意味着我们是不聪明的。 22 | > 23 | > 与看重聪明的人相比,看重努力的人,更有可能实现自己的目标。这个研究表明,被夸奖聪明的孩子,遇到困难时容易沮丧,很快就会放弃;而被夸奖努力的孩子,能够坚持很长时间,并且享受解决问题的过程。 24 | > 25 | > 如果不聪明,那就足够努力吧。 26 | > 27 | > 引用自 @高冷冷 28 | 29 | 刻意练习很重要。这是一个简单的小工具,可以帮助你回顾自己上一次练习时什么时候,练习了多少次,该项目会不断增加新的练习题。练习题主要围绕 Gopher,你也可以构建自己的题库。 30 | 31 | ## 使用步骤 32 | 33 | 1) 克隆项目 34 | 2) 完成一个练习题 35 | 3) 在 `exercise_book.md` 中记录完成的题目 36 | 4) 可以移除自己这次练习的代码,方便下一次练习,也可以保存 37 | 5) 在项目根目录执行 `$ go run cmd/carol.go`,查看练习题统计信息,预览如下 38 | 39 | ```shell 40 | Name Last done Done Level Topics 41 | ---- --------- ---- ----- ------ 42 | data_structure/linked_list 1 day ago 1x medium 数据结构 43 | data_structure/stack 1 day ago 1x medium 数据结构 44 | ---- ---- 45 | 2 2 46 | ``` 47 | 48 | ## 题库 49 | | 分支 | 题库名 | 更新时间 | 50 | | ---- | ------ | ------- | 51 | | main | Go | 2023-08-29 | 52 | 53 | ## 贡献 54 | 欢迎任何人提供自己的练习题库。你可以 `fork` 本仓库,创建新的分支,分支命名建议以某个职位、领域,例如 C++、Docker 等。 55 | 56 | ## 其他 57 | 58 | 1) `$ go run cmd/carol.go -d 7 -l medium -s 2` 支持 3 个可选参数 59 | 1) `-l`: 获取指定的练习难度,练习难度分别为 `easy`、`medium`、`hard`,默认显示所有难度级别 60 | 2) `-s`: 指定排序字段,1: 题目名称排序、2: 上一次完成时间、3: 完成次数,默认上一次完成时间 61 | 3) `-d`: 指定上一次完成时间的时间范围,可以输入任意阿拉伯数字,例如 7 代表统计 7 天内完成过的练习题,默认不限制时间范围 62 | 2) 可以结合艾宾浩斯遗忘曲线 excel 文档来使用,[点击跳转](http://www.xuexili.com/jiyili/1351.html) 63 | 3) 如果你感觉对于某个练习题已经得心应手了,可以挑战更高难度的,本项目中提供的题目没有特定的练习顺序,我们可以选择自己感兴趣的 64 | 4) 刻意练习有 4 点原则 65 | 1) 目标要明确 66 | 2) 做事时要及其专注 67 | 3) 需要及时看到反馈并进行调整 68 | 4) 走出舒适区,习惯孤独 69 | -------------------------------------------------------------------------------- /carol.go: -------------------------------------------------------------------------------- 1 | package carol 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "sort" 12 | "strings" 13 | "text/tabwriter" 14 | "time" 15 | ) 16 | 17 | // ExerciseBook 练习册用于记录已经完成的练习题, 它的内容格式为 `- Y-m-d: foldName` 如下 18 | // - 2023-08-29: data_structure 19 | // - 2023-08-29: concurrency/channel, concurrency/mutex 20 | const ExerciseBook = "exercise_book.md" 21 | 22 | type Practice struct { 23 | Name string 24 | LastDone time.Time 25 | TimesDone int 26 | Level string 27 | Topics []string 28 | } 29 | 30 | // Get 获取所有的编程题, 练习的统计信息 31 | func Get() ([]Practice, error) { 32 | existing, err := getExisting() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | // 解析题目完成记录文件 38 | done, err := getDone() 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | HERE: 44 | for _, d := range done { 45 | for _, e := range existing { 46 | if d.Name == e.Name { 47 | continue HERE 48 | } 49 | } 50 | } 51 | 52 | for i := range existing { 53 | for j := range done { 54 | if existing[i].Name == done[j].Name { 55 | existing[i].TimesDone = done[j].TimesDone 56 | existing[i].LastDone = done[j].LastDone 57 | } 58 | } 59 | } 60 | 61 | return existing, nil 62 | } 63 | 64 | // Print 在标准输出中打印与练习题的统计有关的表格, 显示信息如下. 65 | // 1. 练习题名称 66 | // 2. 练习的次数 67 | // 3. 练习题的难度 68 | // 4. 练习题所属主题 69 | // 5. 上一次完成练习时间, 以天为单位 70 | func Print(practices []Practice, lastDoneDaysAgo int, column int, level string) { 71 | const format = "%v\t%v\t%5v\t%v\t%v\n" 72 | 73 | // 头部 74 | tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) 75 | fmt.Fprintf(tw, format, "Name", "Last done", "Done", "Level", "Topics") 76 | fmt.Fprintf(tw, format, "----", "---------", "----", "-----", "------") 77 | 78 | // 正文 79 | var totalCount int 80 | var practicesCount int 81 | 82 | sortPractices(practices, &column) 83 | for _, p := range practices { 84 | if !show(p, lastDoneDaysAgo) { 85 | continue 86 | } 87 | if level != "" && p.Level != level { 88 | continue 89 | } 90 | 91 | practicesCount++ 92 | totalCount += p.TimesDone 93 | 94 | fmt.Fprintf(tw, format, p.Name, humanize(p.LastDone), fmt.Sprintf("%dx", p.TimesDone), p.Level, strings.Join(p.Topics, ", ")) 95 | } 96 | // 尾部 97 | fmt.Fprintf(tw, format, "----", "", "----", "", "") 98 | fmt.Fprintf(tw, format, practicesCount, "", totalCount, "", "") 99 | 100 | tw.Flush() 101 | } 102 | 103 | type customSort struct { 104 | practices []Practice 105 | less func(x, y Practice) bool 106 | } 107 | 108 | func (x customSort) Len() int { return len(x.practices) } 109 | func (x customSort) Less(i, j int) bool { return x.less(x.practices[i], x.practices[j]) } 110 | func (x customSort) Swap(i, j int) { x.practices[i], x.practices[j] = x.practices[j], x.practices[i] } 111 | 112 | // 根据 column 对结果集进行排序, 次要排序使用练习题名称 113 | func sortPractices(practices []Practice, column *int) { 114 | sort.Sort(customSort{practices, func(x, y Practice) bool { 115 | switch *column { 116 | case 1: 117 | if x.Name != y.Name { 118 | return x.Name < y.Name 119 | } 120 | case 2: 121 | if x.LastDone != y.LastDone { 122 | return x.LastDone.After(y.LastDone) 123 | } 124 | case 3: 125 | if x.TimesDone != y.TimesDone { 126 | return x.TimesDone > y.TimesDone 127 | } 128 | default: 129 | 130 | } 131 | if x.Name != y.Name { 132 | return x.Name < y.Name 133 | } 134 | return false 135 | }}) 136 | } 137 | 138 | // show 根据时间过滤练习题 139 | func show(p Practice, lastDoneDaysAgo int) bool { 140 | if lastDoneDaysAgo < 0 { 141 | return true 142 | } 143 | t := time.Now().Add(-time.Hour * 24 * time.Duration(lastDoneDaysAgo+1)) 144 | return p.LastDone.After(t) 145 | } 146 | 147 | // humanize 格式化为符合人类阅读习惯的时间 148 | func humanize(lastDone time.Time) string { 149 | if lastDone.IsZero() { 150 | return "never" 151 | } 152 | daysAgo := int(time.Since(lastDone).Hours() / 24) 153 | w := "day" 154 | if daysAgo != 1 { 155 | w += "s" 156 | } 157 | return fmt.Sprintf("%d %s ago", daysAgo, w) 158 | } 159 | 160 | // getExisting 获取已存在的所有编程题 161 | func getExisting() ([]Practice, error) { 162 | cmd := exec.Command("go", "list", "-f", "{{.Dir}}", "./...") 163 | 164 | out, err := cmd.Output() 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | cwd, err := os.Getwd() 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | var result []Practice 175 | 176 | for _, line := range strings.Split(string(out), "\n") { 177 | name := strings.TrimPrefix(line, cwd) 178 | name = strings.TrimPrefix(name, "/") 179 | 180 | if name == "" || strings.HasSuffix(name, "cmd") { 181 | continue 182 | } 183 | 184 | l, t, err := parsePractice(name) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | result = append(result, Practice{Name: name, Level: l, Topics: uniq(t)}) 190 | } 191 | return result, err 192 | } 193 | 194 | // uniq 移除重复的练习主题 195 | func uniq(topics []string) []string { 196 | seen := make(map[string]bool) 197 | 198 | var unique []string 199 | 200 | for _, t := range topics { 201 | if _, ok := seen[t]; !ok { 202 | seen[t] = true 203 | unique = append(unique, t) 204 | } 205 | } 206 | 207 | return unique 208 | } 209 | 210 | func parsePractice(name string) (level string, topics []string, err error) { 211 | fn := func(path string, d fs.DirEntry, err error) error { 212 | if filepath.Ext(path) == ".go" { 213 | f, err := os.Open(path) 214 | if err != nil { 215 | return err 216 | } 217 | defer f.Close() 218 | 219 | s := bufio.NewScanner(f) 220 | for s.Scan() { 221 | line := s.Text() 222 | 223 | if strings.HasPrefix(line, "// Level:") { 224 | level = grepLevel(s.Text()) 225 | } 226 | 227 | if strings.HasPrefix(line, "// Topics:") { 228 | topics = append(topics, grepTopics(s.Text())...) 229 | } 230 | } 231 | 232 | if err := s.Err(); err != nil { 233 | return err 234 | } 235 | } 236 | return nil 237 | } 238 | 239 | absPath, err := filepath.Abs(name) 240 | if err != nil { 241 | return "", nil, err 242 | } 243 | 244 | err = filepath.WalkDir(absPath, fn) 245 | if err != nil { 246 | return "", nil, err 247 | } 248 | 249 | return level, topics, err 250 | } 251 | 252 | func grepLevel(line string) string { 253 | _, level, _ := strings.Cut(line, ":") 254 | return strings.TrimSpace(level) 255 | } 256 | 257 | func grepTopics(line string) []string { 258 | _, topicsStr, _ := strings.Cut(line, ":") 259 | topics := strings.Split(topicsStr, ",") 260 | 261 | for i := range topics { 262 | topics[i] = strings.TrimSpace(topics[i]) 263 | } 264 | 265 | return topics 266 | } 267 | 268 | // getDone 从 Carol 中返回已完成的练习题 269 | func getDone() ([]Practice, error) { 270 | var result []Practice 271 | 272 | f, err := os.Open(ExerciseBook) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | bookLineRE := regexp.MustCompile(`^\s*\*\s*([0-9]{4}\-[0-9]{2}\-[0-9]{2}):\s*(.+)$`) 278 | comaRE := regexp.MustCompile(`\s*,\s*`) 279 | 280 | practices := make(map[string]Practice) 281 | 282 | s := bufio.NewScanner(f) 283 | for s.Scan() { 284 | lineParts := bookLineRE.FindStringSubmatch(s.Text()) 285 | if lineParts == nil { 286 | continue 287 | } 288 | 289 | date, practiceStr := lineParts[1], lineParts[2] 290 | doneOn, err := time.Parse("2006-01-02", date) 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | for _, name := range comaRE.Split(practiceStr, -1) { 296 | if name == "" { 297 | continue 298 | } 299 | 300 | name = strings.TrimSpace(name) 301 | 302 | if practice, ok := practices[name]; ok { 303 | practice.TimesDone++ 304 | if doneOn.After(practice.LastDone) { 305 | practice.LastDone = doneOn 306 | } 307 | practices[name] = practice 308 | } else { 309 | practice.Name = name 310 | practice.TimesDone = 1 311 | practice.LastDone = doneOn 312 | practices[name] = practice 313 | } 314 | } 315 | } 316 | if s.Err() != nil { 317 | return nil, s.Err() 318 | } 319 | 320 | for name := range practices { 321 | result = append(result, practices[name]) 322 | } 323 | 324 | return result, nil 325 | } 326 | --------------------------------------------------------------------------------