前几天刚考完研,数学又一次挂掉了,是的,走出考场的瞬间就知道数学挂掉了,若是不设置单科最低或许还有机会。目前准备开始寻找工作,不过因为已经二战,白费了一年工作时间,不清楚还有哪个公司能招人,但这些已经是后续的事情,便想着在过年前把之前没做完的工程给补齐,于是便有了本文。
本作品旨在提供一个学生教务管理系统,主要包括学科管理、班级管理、考试管理、成绩管理、邮件箱 等功能。
作品使用 Golang 语言进行开发,结构上不依赖第三方库,但使用了少量第三方库作为美化(可以取消使用)。一般而言,是前后端分开但在本工程仅仅作为一个共体,后续确实有计划分开设计并使用网络传输,但在那之前先实现一个简单的管理系统。
本文计划分为两个部分:
第一部分是界面逻辑设计,学过数据结构的同学可以简单理解为就是创建了一个树形结构,每一个功能即一个结点,这是第一部分要实现的内容,这一部分在2022年2月开始写,但中途因为复习缘故,到了2022年9月才完成。
第二部分是逻辑功能实现,利用一定的数据结构和算法实现一个基本的文件型数据库作为存储,逐步实现和整合各个逻辑功能
,这一部分自2022年10月开始断续写,在2022年11月完成了数据存储的验证,后续功能实现从2022年12月28日开始。
下面开始第一部分内容。
本工程作品均为本文作者撰写,作品遵循MPLv2.0开源协议。转载本文请标注出处。
| 软件依赖 | 版本类型 | 版本号 |
|---|---|---|
| GoLand | — | 2022.2.3 |
| Golang | — | 1.19 |

1. data目录
该目录下主要存放使用过程中的各种存储数据,包括class、exam、subject、user,可以根据目录名称推测出大概存放的内容,具体存放格式在每个目录下会有一个独特的说明文件(也可能会放在README.md,以后期为准)。
2. log目录
该目录下主要存放的是程序运行中出现的各种操作或异常的日志记录,该日志系统尚且不够完善的地方在于它未能解决跨日问题,这个在目前不是重点,因此保留下来,如果有同学补充了,可以issue一下。
3. main目录
该目录存放的是程序运行中最关键最核心的代码,目录下主要有三个部分,一个是日志系统,一个是主程序,一个是菜单骨架。其中,日志系统采用协程独立运行。主函数主要只包含了一个操作,读取菜单骨架并提供进一步操作的能力,菜单骨架包括了菜单各结点的构成,每个结点包含的子结点,抑或是每个结点执行功能的路由,基本囊括在这一部分中。
4. public目录
该目录下主要存放着功能性的代码,包括各种基础操作和数据存储处理等,以及公有变量、结构等共享和菜单显示内容的定义,屏幕的输入输出函数即在此。



1. 总体架构
2. 日志处理架构

3. 菜单显示架构

1. 总体思路
以主函数为例,开局先检测各种配置,如果正常则继续。绑定端口使用UDP开启日志处理功能,接着进入一个无限循环∞。在该循环中,主要就是不断读取某个结点并通过提示用户输入,根据用户输入进入下一步,即子结点或者是执行对应的功能,类似一棵树,其中日志的传递通过发送UDP消息进行,最初打算使用协程,使用通道传输消息,但是考虑到了其他因素的影响,使用UDP通信更为合适。
2. 日志处理思路
日志处理主要是遵循两个原则,一个是独立,一个是靠谱。独立日志处理指的是能够在处理其他事物的同时完成日志的记录,这一通信过程依赖UDP,关于日志靠谱实际上就是应该确保每一个日志都能准确记录下来,不能因为有错误即丢弃。
3. 菜单架构思路
菜单架构主要依赖于两个部分,一个是结点的预定义,包括这个结点:谁能访问、显示什么内容、下一个能访问哪个、执行什么,这同时也包含了结点的上一级的返回处理,学过数据结构的同学应该都知道怎么做。
不过最初的时候没处理好,最开始没打算使用双向链表,导致后来莫名其妙实现了单链表往上一级走的功能,没错,后来就是突然加上了双向功能,但却不是彻底的双向链表,只能说是用了辅助双向链表来保存结点。
总之,在结点预定义完成后,无限循环只需要一步访问一个结点就能执行对应的操作,并保存上一结点指针,如果操作函数为空则检查有没有下一级结点,当然,需要一定权限才能访问,这些都需要考虑到。
这里指的是能够呈现出菜单样式的逻辑实现,而不是菜单具体功能的逻辑实现。
1. 预定义
// 包含个人信息相关的结构profile := public.StructProfile{IsLogin: false, Permit: public.PermitGuest}// 传递全局的共享变量,通过参数传递share := public.StructShareBase{Profile: &profile}// 构造一棵树 - 菜单,本质是根节点menu := InitTreeMenu(&share)// 链式列表,保存上一级、下一级和当前结点nodeList := &public.StructMenuLink{Node: menu}// 初始化打印菜单FuncMenuPrint(menu, &share)
2. 引导用户输入【无限循环内】
option := public.SelectOption(len(menu.MenuNode))if option < 0 {continue}
3. 返回上一级操作【无限循环内】
if option == 0 {// 若上一级不为空则将上一级的链点赋给当前链点// 并传递菜单属性,否则退出并结束程序if nodeList.NodeLast != nil {nodeList = nodeList.NodeLastmenu = nodeList.Node} else {public.FuncPrintLog(public.LogInfo, "程序正在退出...")public.Clear()public.TipWait("正在退出")public.Clear()return}
4. 进入下一级/执行操作【无限循环内】
} else {// 判断下一链点是否为空,如果为空则创建链点// 并将下一链点的`NodeLast`指向为当前链点// 同时`nodeList`指针指向下一链点if nodeList.NodeNext == nil {node := &public.StructMenuLink{NodeLast: nodeList}nodeList = node}// 对当前菜单指向用户所选菜单选项// 并将指向后的菜单属性赋予`nodeList`链点的菜单属性`Node`menu = menu.MenuNode[option-1]nodeList.Node = menu// 执行当前菜单附带的操作函数,若需要验证则传入验证操作量// 通过后允许进入菜单,否则返回上一级菜单,或者保持当前菜单if menu.Func != nil {if menu.HasVerifier {vfResult := falsemenu.Func(&share, &vfResult)if !vfResult {nodeList = nodeList.NodeLastmenu = nodeList.Node}} else if menu.IsKeepMenu {menu.Func(&share)nodeList = nodeList.NodeLastmenu = nodeList.Node} else {menu.Func(&share)}}}
1. 端口监听
func ServerRun() {log = getLogFile("./log")server, err := net.ResolveUDPAddr("udp4", "127.0.0.1:32111")if err != nil {public.TipWait("开启日志系统时出现错误,即将关闭系统")os.Exit(0)}conn, err := net.ListenUDP("udp", server)for {disposeSocket(conn)}
}
2. 消息处理
func disposeSocket(conn *net.UDPConn) {var dataByte [1024]bytecount, _, err := conn.ReadFromUDP(dataByte[:])if err != nil {public.TipWait("接收日志操作请求时出现错误")return}go logWrite(dataByte[:count])
}
3. 日志写入
func logWrite(data []byte) {// TODO 尚未解决跨日问题,可以通过在此调用查询文件名是否匹配当天日期而解决if log == nil {return}mutex.Lock()timeStr := time.Now().Format("[2006-01-02 15:04:05] ")_, err := log.WriteString(timeStr)_, err = log.Write(data)_, err = log.Write([]byte{'\r', '\n'})err = log.Sync()if err != nil {mutex.Unlock()return}mutex.Unlock()
}
4. 日志文件打开方式
func getLogFile(path string) *os.File {var logFile *os.FilefileName := time.Now().Format("2006-01-02") + ".log"filePath := path + "/" + fileNameflag := isFileExist(filePath)if flag {// 当天日志已存在就追加logFileTemp, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0644)if err != nil {return nil}logFile = logFileTemp} else {// 当天日志不存在就创建logFileTemp, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)if err != nil {return nil}logFile = logFileTemp}return logFile
}
1. 用户信息
type StructProfile struct {IsLogin boolName stringUserId stringClassId stringSubjectId stringPermit int
}
2. 菜单结构
type StructMenu struct {Title stringProfile *StructProfilePermitMode intPermitList []intText stringHasVerifier boolIsKeepMenu boolMenuNode []*StructMenuFunc func(*StructShareBase, ...any)
}
3. 结点相互结构(双向链表)
type StructMenuLink struct {NodeLast *StructMenuLinkNodeNext *StructMenuLinkNode *StructMenu
}
4. 共享变量
type StructShareBase struct {Profile *StructProfile
}
1. 根节点
func InitTreeMenu(share *public.StructShareBase) *public.StructMenu {return &public.StructMenu{Profile: share.Profile,PermitList: []int{public.PermitGuest},Text: public.Menu00Login,MenuNode: []*public.StructMenu{&nodeLogin,&nodeRegister,&nodeChangeAccount,},}
}
2. 一级菜单
var nodeLogin = public.StructMenu{PermitMode: public.PermitModeEqualGreater,PermitList: []int{public.PermitGuest},Text: public.Menu00Login,HasVerifier: true,MenuNode: []*public.StructMenu{&nodeProfile,&nodeSubject,&nodeClass,&nodeExam,&nodeReport,&nodeMailbox,&nodeToolbox,},Func: funcLogin,
}var nodeRegister = public.StructMenu{PermitMode: public.PermitModeEqualGreater,PermitList: []int{public.PermitGuest},Text: public.Menu00Register,Func: funcRegister,
}var nodeChangeAccount = public.StructMenu{PermitMode: public.PermitModeEqualGreater,PermitList: []int{public.PermitGuest},HasVerifier: true,IsKeepMenu: true,Text: public.Menu00ChangeAccount,Func: funcChangeAccount,
}
3. 二级菜单
var nodeProfile = public.StructMenu{PermitMode: public.PermitModeGreater,PermitList: []int{public.PermitGuest},Text: public.Menu01Profile,MenuNode: []*public.StructMenu{&nodeProfileSelfCheck,&nodeProfileSelfModify,&nodeProfileOtherCheck,&nodeProfileOtherEdit,},
}var nodeSubject = public.StructMenu{PermitMode: public.PermitModeGreater,PermitList: []int{public.PermitUser},Text: public.Menu01Subject,MenuNode: []*public.StructMenu{&nodeSubjectAdd,&nodeSubjectDel,&nodeSubjectCheck,&nodeSubjectEdit,},
}var nodeClass = public.StructMenu{PermitMode: public.PermitModeGreater,PermitList: []int{public.PermitGuest},Text: public.Menu01Class,MenuNode: []*public.StructMenu{&nodeClassAdd,&nodeClassDel,&nodeClassCheck,&nodeClassEdit,&nodeClassMemberAdd,&nodeClassMemberDel,},
}var nodeExam = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu01Exam,MenuNode: []*public.StructMenu{&nodeExamAdd,&nodeExamDel,&nodeExamCheck,&nodeExamEdit,},
}var nodeReport = public.StructMenu{PermitMode: public.PermitModeGreater,PermitList: []int{public.PermitGuest},Text: public.Menu01Report,MenuNode: []*public.StructMenu{&nodeReportAdd,&nodeReportEdit,&nodeReportCheckDate,&nodeReportCheckId,},
}var nodeMailbox = public.StructMenu{PermitMode: public.PermitModeGreater,PermitList: []int{public.PermitGuest},Text: public.Menu01Mailbox,MenuNode: []*public.StructMenu{&nodeMailboxSend,&nodeMailboxReceive,},
}var nodeToolbox = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu01Toolbox,MenuNode: []*public.StructMenu{&nodeToolboxAddActiveCode,&nodeToolboxDelAccount,},
}
4. 三级菜单
var nodeProfileSelfCheck = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager, public.PermitUser},Text: public.Menu02ProfileSelfCheck,Func: funcProfileSelfCheck,
}var nodeProfileSelfModify = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager, public.PermitUser},Text: public.Menu02ProfileSelfModify,Func: funcProfileSelfModify,
}var nodeProfileOtherCheck = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ProfileOtherCheck,Func: funcProfileOtherCheck,
}var nodeProfileOtherEdit = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ProfileOtherEdit,Func: funcProfileOtherEdit,
}var nodeSubjectAdd = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02SubjectAdd,Func: funcSubjectAdd,
}var nodeSubjectDel = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02SubjectDel,Func: funcSubjectDel,
}var nodeSubjectCheck = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager, public.PermitUser},Text: public.Menu02SubjectCheck,Func: funcSubjectCheck,
}var nodeSubjectEdit = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02SubjectEdit,Func: funcSubjectEdit,
}var nodeClassAdd = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ClassAdd,Func: funcClassAdd,
}var nodeClassDel = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ClassDel,Func: funcClassDel,
}var nodeClassCheck = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager, public.PermitUser},Text: public.Menu02ClassCheck,Func: funcClassCheck,
}var nodeClassEdit = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ClassEdit,Func: funcClassEdit,
}var nodeClassMemberAdd = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ClassMemberAdd,Func: funcClassMemberAdd,
}var nodeClassMemberDel = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ClassMemberDel,Func: funcClassMemberDel,
}var nodeExamAdd = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ExamAdd,Func: funcExamAdd,
}var nodeExamDel = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ExamDel,Func: funcExamDel,
}var nodeExamCheck = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ExamCheck,Func: funcExamCheck,
}var nodeExamEdit = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ExamEdit,Func: funcExamEdit,
}var nodeReportAdd = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ReportAdd,Func: funcReportAdd,
}var nodeReportEdit = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ReportEdit,Func: funcReportEdit,
}var nodeReportCheckDate = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ReportCheckDate,Func: funcReportCheckDate,
}var nodeReportCheckId = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager},Text: public.Menu02ReportCheckId,Func: funcReportCheckId,
}var nodeMailboxSend = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager, public.PermitUser},Text: public.Menu02MailboxSend,Func: funcMailboxSend,
}var nodeMailboxReceive = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator, public.PermitManager, public.PermitUser},Text: public.Menu02MailboxReceive,Func: funcMailboxReceive,
}var nodeToolboxAddActiveCode = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ToolboxAddActiveCode,Func: funcToolboxAddActiveCode,
}var nodeToolboxDelAccount = public.StructMenu{PermitMode: public.PermitModeEqual,PermitList: []int{public.PermitAdministrator},Text: public.Menu02ToolboxDelAccount,Func: funcToolboxDelAccount,
}
这一部分在下一节讲述。
func checkArgs(option []any) bool {}
func funcLogin(share *public.StructShareBase, option ...any) {}
func funcRegister(share *public.StructShareBase, _ ...any) {}
func funcChangeAccount(share *public.StructShareBase, _ ...any) {}
func funcProfileSelfCheck(share *public.StructShareBase, _ ...any) {}
func funcProfileSelfModify(share *public.StructShareBase, _ ...any) {}
func funcProfileOtherCheck(share *public.StructShareBase, _ ...any) {}
func funcProfileOtherEdit(share *public.StructShareBase, _ ...any) {}
func funcSubjectAdd(share *public.StructShareBase, _ ...any) {}
func funcSubjectDel(share *public.StructShareBase, _ ...any) {}
func funcSubjectCheck(share *public.StructShareBase, _ ...any) {}
func funcSubjectEdit(share *public.StructShareBase, _ ...any) {}
func funcClassAdd(share *public.StructShareBase, _ ...any) {}
func funcClassDel(share *public.StructShareBase, _ ...any) {}
func funcClassCheck(share *public.StructShareBase, _ ...any) {}
func funcClassEdit(share *public.StructShareBase, _ ...any) {}
func funcClassMemberAdd(share *public.StructShareBase, _ ...any) {}
func funcClassMemberDel(share *public.StructShareBase, _ ...any) {}
func funcExamAdd(share *public.StructShareBase, _ ...any) {}
func funcExamDel(share *public.StructShareBase, _ ...any) {}
func funcExamCheck(share *public.StructShareBase, _ ...any) {}
func funcExamEdit(share *public.StructShareBase, _ ...any) {}
func funcReportAdd(share *public.StructShareBase, _ ...any) {}
func funcReportEdit(share *public.StructShareBase, _ ...any) {}
func funcReportCheckDate(share *public.StructShareBase, _ ...any) {}
func funcReportCheckId(share *public.StructShareBase, _ ...any) {}
func funcMailboxSend(share *public.StructShareBase, _ ...any) {}
func funcMailboxReceive(share *public.StructShareBase, _ ...any) {}
func funcToolboxAddActiveCode(share *public.StructShareBase, _ ...any) {}
func funcToolboxDelAccount(share *public.StructShareBase, _ ...any) {}
1. 初始化UDP客户端
func InitUdpClient() {conn, err := net.ListenPacket("udp", ":0")if err != nil {log.Fatal(err)}dst, err := net.ResolveUDPAddr("udp", "127.0.0.1:32111")if err != nil {log.Fatal(err)}socketDst = dstsocket = conn
}
2. 字符串转Byte
func FuncStringToByteSlice(s string) []byte {tmp1 := (*[2]uintptr)(unsafe.Pointer(&s))tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}return *(*[]byte)(unsafe.Pointer(&tmp2))}
3. 日志打印
func FuncPrintLog(level byte, info string, args ...error) {var stringErr = "Undefined EOF..."if 0 < len(args) && args[0] != nil {stringErr = fmt.Sprintf("%v", args[0])}if openDebug {if 0 == len(args) {fmt.Printf("[DEBUG] [INFO] %s\n", info)} else {fmt.Printf("[DEBUG] [ERRS] %s --> %s\n", info, stringErr)}time.Sleep(3 * time.Second)}if socket == nil || socketDst == nil {fmt.Printf("\n[ERROR] 发送数据时建立链接失败,套接字对象为空,数据内容:%s\n", info)time.Sleep(10 * time.Second)return}switch level {case LogErrs:data := "[ERROR] " + infoif 0 != len(args) {data = data + args[0].Error()}_, err := socket.WriteTo(FuncStringToByteSlice(data), socketDst)if err != nil {fmt.Printf("\n发送数据失败,原始错误信息:%s,日志无法被记录,err:%v\n", stringErr, err)time.Sleep(10 * time.Second)return}breakcase LogWarn:data := "[WARN] " + info_, err := socket.WriteTo(FuncStringToByteSlice(data), socketDst)if err != nil {fmt.Printf("\n发送数据失败,日志无法被记录,err:%v\n", err)time.Sleep(10 * time.Second)return}breakcase LogInfo:data := "[INFO] " + info_, err := socket.WriteTo(FuncStringToByteSlice(data), socketDst)if err != nil {fmt.Printf("\n发送数据失败,日志无法被记录,err:%v\n", err)time.Sleep(10 * time.Second)return}breakdefault:}}
4. 打印并等待若干时间
func TipWait(str string, timeSleep ...int) {fmt.Print(str)timer := 3if len(timeSleep) != 0 && 0 < timeSleep[0] {timer = timeSleep[0]}for i := 0; i < timer; i++ {fmt.Print(".")time.Sleep(1 * time.Second)}fmt.Println()
}
5. 清屏
func Clear() {var cmd *exec.Cmdswitch SysType {case "linux":cmd = exec.Command("clear")breakcase "windows":cmd = exec.Command("cmd", "/c", "cls")breakdefault:FuncPrintLog(LogWarn, "没有对应的系统类型,无法清屏")return}cmd.Stdout = os.Stdouterr := cmd.Run()if err != nil {FuncPrintLog(LogWarn, "操作系统无法完全匹配,无法清屏")}
}
6. 引导选择选项
func SelectOption(rangeMax int) int {var option intTip("请选择菜单选项:")_, err := fmt.Scanln(&option)if err != nil {Tip(Format(TipOpFail, "输入内容非法!请重新输入:"))return -1}if rangeMax < option || option < 0 {Tip(Format(TipOpFail, "请选择范围内的选项:"))return -1}return option
}
7. 引导输入
func TipInput(tip string, verifier ...func(string) (bool, string)) (string, error) {var input stringfor {Clear()fmt.Println(tip)_, err := fmt.Scanln(&input)if err != nil {TipWait(Format(TipOpFail, "输入内容非法!请重新输入!"))return "", err}if len(verifier) == 0 || verifier[0] == nil {return input, nil}vf := verifier[0]vfFlag, tips := vf(input)if vfFlag {break}TipWait(tips)}return input, nil
}
8. 格式化输入
提供类似Sprintf()的操作。
func Format(format string, strList ...string) string {var builder strings.BuildercountLengthStrList := 0lenList := len(strList)for _, str := range strList {countLengthStrList += len(str)}// 计算容量并预分配builder.Grow(countLengthStrList + len(format) - lenList*2)count := 0start := 0lastByte := int32(format[0])for i, bt := range format {if i == 0 {continue}if count == lenList {builder.WriteString(format[i:])break}if lastByte == '%' && bt == 's' {for j := start; j < i-1; j++ {builder.WriteByte(format[j])}//builder.WriteString(format[start : i-1])builder.WriteString(strList[count])count++start = i + 1}lastByte = bt}return builder.String()
}
没能考上还是挺可惜的,游戏玩的也无味,属于自己玩就不想玩的那种,所幸还有不少要做的事情,或许之后再考虑考一次吧,更希望有机会接触和学习到顶端的技术和知识。本文只是第一篇,关于结构上的实现。下一篇是逻辑功能的具体实现,希望尽早能弄完。
提前祝大家元旦快乐!!
Go-NoUiStudentManage
[1] Go 并发 | 菜鸟教程
[2] CLI Color | Github
END
下一篇:CSS3知识点精学