六. 开始编写你的程序 DRAFT
常用的IDE
下面是一些比较常见的好的编程 IDE:
-
Eclipse:目前最流行的 Java IDE 之一,支持多种编程语言和框架,功能强大。
-
IntelliJ IDEA:JetBrains 公司开发的 Java IDE,支持多种编程语言和框架,作为 Android Studio 标准 IDE 使用。
-
Visual Studio:微软开发的 IDE,支持多种编程语言,非常强大,同时也支持多种操作系统。
-
Visual Studio Code:微软开发的轻量级代码编辑器,支持多种编程语言,可通过安装插件来拓展功能。
-
Sublime Text:跨平台的代码编辑器,速度快,支持多种编程语言,支持多种插件。
-
Atom:GitHub 开源的跨平台代码编辑器,支持多种编程语言和框架,支持多种插件。
这些 IDE 都有其自身的优点和适用范围,可以根据自己的需求和使用习惯选择合适的 IDE。
macOS搭建PyCharm的python开发环境
在 macOS 上搭建 PyCharm 的 Python 开发环境需要以下步骤:
-
安装 Python: 在官网 https://www.python.org/downloads/ 下载最新版的 Python 并安装。
-
安装 PyCharm: 在 JetBrains 官网 https://www.jetbrains.com/pycharm/download 下载适合macOS的安装包,并安装。
-
配置 Python 解释器: 打开 PyCharm,点击 Preferences(或者按下快捷键 Command + ,),在左侧栏找到 “Project:xxx -> Project Interpreter”,点击 New Environment 即可自动安装并配置 Python 解释器。
-
新建项目: 在 PyCharm 中,选择 File -> New Project,为项目选择一个名称并选择刚刚配置的 Python 解释器即可。
-
编写代码: 在 PyCharm 中,可以方便地创建新的 Python 文件,并在其中编写代码,然后通过运行按钮来运行程序。
这样,你就可以在 macOS 上搭建 PyCharm 的 Python 开发环境了!
macOS搭建VS Code的python开发环境
下面是在 macOS 上搭建 VS Code 的 Python 开发环境的步骤:
-
安装 Python: 在官网 https://www.python.org/downloads/ 下载最新版的 Python 并安装。
-
安装 VS Code: 在官网 https://code.visualstudio.com/ 下载 macOS 版本的 VS Code 安装包,并安装。
-
安装 Python 扩展: 在 VS Code 中,点击左侧的 Extensions 图标(或者按下快捷键 Command + Shift + X),在搜索框中搜索 Python 并安装。
-
配置 Python 解释器: 打开 VS Code,按下快捷键 Command + Shift + P,输入 “Python: Select Interpreter” 并选择需要的解释器。
-
创建新的 Python 文件: 在 VS Code 中创建一个新的 Python 文件,可以通过快捷键 Command + N 或者选择菜单 File -> New File。
-
编写代码: 在新建的 Python 文件中编写代码,可以使用 Python 扩展提供的功能进行高亮、自动补全等操作。
-
运行代码: 在 VS Code 的终端中输入想要运行的 Python 文件名,可以通过快捷键 Control + Shift + P,选择 “Run Python File in Terminal” 也可以运行。
这样,你就可以在 macOS 上搭建 VS Code 的 Python 开发环境了!
macOS搭建Goland的go语言开发环境
下面是在 macOS 上搭建 GoLand 的 Go 语言开发环境的步骤:
-
安装 Go: 在官网 https://golang.org/dl/ 下载最新版的 Go 并安装。
-
安装 GoLand: 在官网 https://www.jetbrains.com/go/download/ 下载适合 macOS 的安装包,并安装。
-
配置 GOPATH: 打开终端,输入以下命令:
mkdir $HOME/go echo 'export GOPATH=$HOME/go' >> ~/.bash_profile echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.bash_profile source ~/.bash_profile
这样就配置了 GOPATH 环境变量。
-
配置 GoLand: 打开 GoLand,点击 Configure -> Preferences(或者按下快捷键 Command + ,),在左侧栏中找到 “Go -> Go Libraries”,点击 “+” 添加 GOPATH。
-
创建新项目: 在 GoLand 中,选择 File -> New Project,为项目选择一个名称并选择刚刚配置好的 GOPATH 环境即可。
-
编写代码: 在 GoLand 中,可以方便地创建新的 Go 文件,并在其中编写代码。
-
运行代码: 在 GoLand 中,可以使用 Run 按钮或者快捷键 Control + Shift + R 来运行代码。
这样,你就可以在 macOS 上搭建 GoLand 的 Go 语言开发环境了!
macOS搭建VS Code的go语言开发环境
好的,下面是在 macOS 上搭建 VS Code 的 Go 语言开发环境的步骤:
-
安装 Go: 在官网 https://golang.org/dl/ 下载最新版的 Go 并安装。
-
安装 VS Code: 在官网 https://code.visualstudio.com/ 下载 macOS 版本的 VS Code 安装包,并安装。
-
安装 Go 扩展: 在 VS Code 中,点击左侧的 Extensions 图标(或者按下快捷键 Command + Shift + X),在搜索框中搜索 Go 并安装。
-
配置 GOPATH 环境变量: 打开终端并输入以下命令:
mkdir $HOME/go echo 'export GOPATH=$HOME/go' >> ~/.bash_profile echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.bash_profile source ~/.bash_profile
如果你使用的是 zsh,可将 .bash_profile 替换为 .zshrc。
-
配置 Go 扩展: 在 VS Code 中,按下快捷键 Command + Shift + P,输入 “Go: Configure Workspace Settings”,找到 “go.gopath” 选项并设置为刚刚配置的 GOPATH。
-
创建新项目: 在 VS Code 中,选择菜单 File -> New Folder,为项目选择一个名称。点击左下方的 “Go” 按钮,选择 “Create New Go Project” 即可创建一个新的 Go 项目。
-
编写代码: 在新建的 Go 项目中,可以使用快捷键 Command + N 创建新的 Go 文件,并在其中编写代码。
-
运行代码: 在 VS Code 中,可以使用 Run 按钮或者快捷键 Control + Shift + R 来运行代码。
这样,你就可以在 macOS 上搭建 VS Code 的 Go 语言开发环境了!
macOS搭建IDEA的java语言开发环境
好的,下面是在 macOS 上搭建 IntelliJ IDEA 的 Java 语言开发环境的步骤:
-
安装 JDK: 在官网 https://www.oracle.com/java/technologies/javase-downloads.html 下载最新版的 JDK 并安装。
-
安装 IntelliJ IDEA: 在官网 https://www.jetbrains.com/idea/download/ 下载适合 macOS 系统的 IntelliJ IDEA 安装包,并安装。
-
配置环境变量: 打开终端,输入以下命令:
echo 'export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home' >> ~/.bash_profile echo 'export PATH=$PATH:$JAVA_HOME/bin' >> ~/.bash_profile source ~/.bash_profile
如果你使用的是 zsh,可将 .bash_profile 替换为 .zshrc。
-
创建新项目: 在 IntelliJ IDEA 中,选择 File -> New -> Project,为项目选择一个名称并选择 JDK 即可。
-
编写代码: 在 IntelliJ IDEA 中,可以方便地创建新的 Java 文件,并在其中编写代码。
-
运行代码: 在 IntelliJ IDEA 中,可以使用 Run 按钮或者快捷键 Shift + F10 来运行代码。
这样,你就可以在 macOS 上搭建 IntelliJ IDEA 的 Java 语言开发环境了!
开始
通过前面的需求分析和概要设计, 已经基本清楚要做什么事情了, 现在我们要开始学习计算机语言并编写现阶段场景条件下的控制台字符界面程序; 在开始写程序之前, 早些时候, 软件工程要求要做一个详细设计, 要编写一大堆文档描述每个程序步骤…, 随着时代的发展和意识到编写这份文档过程中造成的人力物力浪费, 现在都是敏捷开发或者前面说过的"基于UI编程", 敏捷开发是什么意思? 下面是百度的话:
百度: 敏捷开发以用户的需求进化为核心,采用迭代、循序渐进的方法进行开发。在敏捷开发中,软件项目在构建初期被切分成多个子项目,各个子项目的成果都经过测试,具备可视、可集成和可运行使用的特征。换言之,就是把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可用状态。
说简单一点, 就是不再做详细设计, 而是应对各项需求把整个系统分成几个可集成的部分分别完成, 快速动手并时刻保持可运行使用的状态, 不断迭代(这里迭代的意思是: 每次加一点新的功能进去) 地开发软件… 对, 就是这个意思[龇牙]
我们是要学习编程, 所以在过程中, 前期我会对各个功能画流程图, 这原本是属于详细设计的内容, 现在正式项目中没人画这东西了, 但它是初期学习编程的好手段, 有时候对于解决复杂的问题,草纸上画画也是挺有用的…
我们要做的是学生成绩管理系统,所以先给程序起个名字; 名字要表现出来我们这个程序的主要功能 => Student Score Management System, 所以我们创建的代码工程(project)就叫ssms
其实这类系统有一个统称:MIS ( Management Information System )
现在你的角色是一个程序员, 你要用架构师确定的技术和业务框架来完成目标, 通常在开始的时候会由一个技术出众经验丰富的程序员(一般称为主程–主要程序员的意思)来搭建一个"框架", 框架在软件开发中框架的意思是半成品的意思, 说白一点就是画个骨架再打个样, 多人协同的时候各自负责一块往里面填肉最后一起完成整个程序.
现在你就是主程, 我们一边学习编程语言, 一边创建工程, 然后按照设计把功能划分好, 再做登录模块和用户管理–用它来**“打个样”**, 下面用go语言做例子, 详细地把主程创建框架做登录模块的过程完整地做一遍, 在这个过程中你会学到:
- 变量
- 计算机的内存数据
- 内存地址
- 进制转换
- 基本数据类型
- 逻辑控制
- 条件判断
- 循环控制
- 函数
- 程序包 – 不要重复发明轮子
- 数据的集合
- 数据的抽象
- 文件存储
我们先来画一下登录的流程, 然后一步步实现它
st=>start: 开始
loginui=>operation: 显示登录界面,提示用户输入用户名密码
loginerr=>operation: 提示错误,是否重试
inputuser=>inputoutput: 用户输入用户名和密码
velidate=>condition: 用户校验
retrylogin=>condition: 选择重试?
mainmenu=>subroutine: 主菜单界面
ed=>end: 结束
st->loginui->inputuser->velidate
velidate(no)->loginerr->retrylogin
retrylogin(yes, right)->loginui
retrylogin(no, left)->ed
velidate(yes)->mainmenu
go语言, python语言, Java语言都能完成这部分工作, 我们用go语言做例子进行讲解, 因为它被称为: 21世纪的…
下面看我一步一步做, 过程中会不断迭代(回过去重做)你会体会到代码是怎么越变越少,框架是怎样一点一点建立的, 当然, 在经验多了以后就不会这么麻烦, 会在一开始就知道要怎样做这个骨架的, 期待你..
打开 goLand 新建一个工程, New Project 对话框中, 左边工程类型选Go, 右边Location(工程位置)选择一个目录, 路径的最后是工程的名字, 我们起名ssms
这里说明一下对文件夹的管理
6.1 程序里的变量
在编程中,变量是用于存储和操作数据的一种容器,它们可以在程序中处理不同类型的数据。变量可以保存不同类型的数据,如数字、字符串、布尔值等,并将其在程序中使用。
在程序中,变量常常像一个具有名称的盒子,里面存放着一个特定的值。当一个程序中使用一个变量时,它就提供了一个名称以及相关的数据类型和存储位置(内存地址)。然后程序可以在任何需要使用该变量时引用该变量,并读取或修改它的值。
例如,一个简单的程序,用变量x存储一个数字值,并用变量y存储一个字符串值,如下所示:
x = 5
y = "Hello World!"
print(x)
print(y)
在这个例子中,我们创建了两个变量x
和y
,并分别赋值为一个整数和一个字符串。在最后两行的 print
语句中,我们使用变量x
和y
引用他们对应的值,并将其打印到终端上。
在大部分编程语言中,变量都包括三个要素:变量名、变量类型和变量值。
-
变量名:用于标识一个变量,在编程中,变量名往往是一个具有意义的名称。
-
变量类型:用于确定一个变量可以存储哪种类型的数据,例如整数、浮点数、布尔值、字符串等。不同的编程语言支持的变量类型各不相同。
-
变量值:变量值是存储在变量中的实际数据。当变量被创建时,它可以被赋予一个初值,或在程序执行过程中被不断更新修改。
在许多编程语言中,定义变量的方式是使用赋值语句,例如“a = 1”这个语句将值1存储在名为a的变量中。此后,当程序需要使用变量a时,只需使用这个变量名即可。例如,在计算数学方程时,可以使用变量来存储中间结果,以及最终的结果:
x = 2
y = 3
z = x + y # variable z stores the sum of x and y
不同类型的变量也支持各种操作和函数,例如数学运算、字符串连接、列表操作等。理解变量的基本概念,可以使程序员更有效地组织和管理程序中的数据,是编程中的基础知识之一。
6.2 计算机中的内存数据
6.3 内存地址
内存地址是计算机内存中唯一标识某个变量或数据的一个整数值,通常用十六进制数来表示。计算机内存可以看做是一系列连续的内存单元,每个内存单元都拥有一个唯一的内存地址。
在程序中,变量被存储在内存中的某个地址中,我们可以通过访问这个地址来读取或修改变量的值。计算机的硬件和操作系统通常提供了内存管理功能,使得程序员无需关心内存地址的具体分配和释放过程,而只需要通过编程语言提供的基本操作来处理内存中的数据。
在大部分编程语言中,我们可以通过指针或引用来操作内存地址。指针是一个变量,它存储了另一个变量的内存地址,可以通过指针访问内存中的数据;引用则是一种更加高级的指针,它隐藏了底层的内存地址,并提供了更加安全和易用的访问方式。
在使用指针或引用时,需要注意内存管理的安全性和一致性,避免出现野指针或内存泄漏等问题。因此,在程序设计中,通常需要仔细考虑内存管理的方案。
6.4 进制转换
基本数据类型
C语言
C语言中的数据类型可以分为四类:基本类型、枚举类型、指针类型和结构体类型。
- 基本类型:C语言中的基本类型包括整型、浮点型和字符型,具体如下:
- 整型:char、short、int、long、long long
- 浮点型:float、double、long double
- 字符型:char
这些基本类型的变量直接存储的是它们所对应的值。
- 枚举类型:枚举类型(enum)是一种用户定义的类型,用于表示一组固定的取值范围,被定义为一组有名字的常量。例如:
enum Color { RED, GREEN, BLUE };
-
指针类型:指针类型(pointer)用于存储变量的地址,指向存储在内存中的某个位置。需要注意的是,指针必须先被初始化,否则它的值是不确定的。
-
结构体类型:结构体类型(struct)用于表示一组相关的数据,可以包括不同类型的数据,类似于一个自定义的复合类型。例如:
struct Point {
int x;
int y;
};
这些数据类型可以用于定义变量、函数参数、返回值等,是构建C程序的基础。
Go语言
Go语言的基本数据类型包括以下几种:
- bool:布尔类型,值为 true 或 false。
- string:字符串类型,由一串字符组成。
- int:整型,有 int8、int16、int32、int64 四种类型,分别表示 8、16、32、64 位有符号整数。
- uint:无符号整型,有 uint8、uint16、uint32、uint64 四种类型,分别表示 8、16、32、64 位无符号整数。
- rune:字符型,有 int32 的别名,用于表示 Unicode 字符。
- byte:字节型,有 uint8 的别名,用于存储二进制文件或者图片等字节流数据。
- float:浮点型,有 float32、float64 两种类型,分别表示单精度和双精度浮点数。
- complex:复数类型,有 complex64、complex128 两种类型,分别表示实部和虚部均为 float32 和 float64 的复数。
这些数据类型可以用于定义变量、函数参数、结构体等,是编写Go程序的基础。
Pyhton语言
Python语言中的数据类型包括以下几类:
- 数字类型:包括整数类型(int)、浮点数类型(float)、复数类型(complex)。
- 布尔类型:布尔类型(bool)用于表示真或假,只有两个值:True和False。
- 字符串类型:字符串类型(str)用于表示文本数据,可以包含任意的字符,用一对单引号(’)或双引号(”)括起来。
- 列表类型:列表类型(list)用于表示一组有序的数据,可以包含任何类型的数据,用一对方括号([])括起来。
- 元组类型:元组类型(tuple)也用于表示一组有序的数据,但是不可修改,用一对小括号(())括起来。
- 字典类型:字典类型(dict)用于表示一组键值对,键必须是唯一的,用一对大括号({})括起来。
- 集合类型:集合类型(set)用于表示一组无序的、唯一的数据,用一对大括号({})或set()函数来创建。
Python还支持一些其他的数据类型,比如None(表示空值)、bytes(表示字节流数据)、bytearray(和bytes相似但是可修改)、等等。这些数据类型可以用于定义变量、函数参数、类属性等,是构建Python程序的基础。
Java语言
Java语言中的数据类型可以分为两种:基本类型和引用类型。
- 基本类型:Java中的基本类型包括整型、浮点型、字符型、布尔型和字节型,具体如下:
- 整型:byte、short、int、long
- 浮点型:float、double
- 字符型:char
- 布尔型:boolean
这些基本类型的变量直接存储的是它们所对应的值,而不是指向某个对象的引用。
- 引用类型:Java中的引用类型包括类、接口、数组等。引用类型的变量存储的是对象的引用,而不是对象本身。Java中的每个对象都是通过引用来访问的。
Java中还有一些特殊的类型,如void类型表示无返回值,String类型表示字符串等。
这些数据类型可以用于定义变量、函数参数、返回值等,是构建Java程序的基础。
7. 逻辑控制
go语言的逻辑控制语句
Go语言的逻辑控制语句包括以下几种:
- if语句:用于条件判断。例如:
if x > 10 {
// do something
} else {
// do something else
}
- switch语句:用于多分支选择。例如:
switch x {
case 1:
// do something
case 2:
// do something else
default:
// do something if no case matches
}
- for语句:用于循环执行一段代码。例如:
for i := 0; i < 10; i++ {
// do something
}
- range语句:用于遍历数组、切片、字典、通道等数据结构。例如:
for index, value := range array {
// do something with index and value
}
- break和continue语句:用于跳出循环或者继续执行循环。例如:
for i := 0; i < 10; i++ {
if i == 5 {
break // break out of the loop when i == 5
}
if i % 2 == 0 {
continue // skip even numbers
}
// do something with odd numbers before i == 5
}
这些逻辑控制语句可以帮助我们编写复杂的程序,实现不同的流程控制逻辑。
python语言的逻辑控制语句
Python语言的逻辑控制语句包括以下几种:
- if语句:用于条件判断。例如:
if x > 10:
# do something
elif x < 0:
# do something else
else:
# do something if no condition is met
- while循环:用于循环执行一段代码,直到条件不成立。例如:
while x > 0:
# do something
x -= 1
- for循环:用于遍历数组、列表、字典等数据结构。例如:
for i in range(10):
# do something with i
for item in list:
# do something with item
for key, value in dict.items():
# do something with key and value
- break和continue语句:用于跳出循环或者继续执行循环。例如:
for i in range(10):
if i == 5:
break # break out of the loop when i == 5
if i % 2 == 0:
continue # skip even numbers
# do something with odd numbers before i == 5
- try-except语句:用于捕获并处理异常。例如:
try:
# some code that may cause an exception
result = 1 / 0
except ZeroDivisionError:
# do something when an exception is caught
result = 0
这些逻辑控制语句可以帮助我们编写复杂的程序,实现不同的流程控制逻辑,以及异常处理等功能。
Java语言的逻辑控制语句
Java语言的逻辑控制语句包括以下几种:
- if语句:用于条件判断。例如:
if (x > 10) {
// do something
} else if (x < 0) {
// do something else
} else {
// do something if no condition is met
}
- while循环:用于循环执行一段代码,直到条件不成立。例如:
while (x > 0) {
// do something
x--;
}
- for循环:用于遍历数组、列表、集合等数据结构。例如:
for (int i = 0; i < 10; i++) {
// do something with i
}
for (String item : list) {
// do something with item
}
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// do something with key and value
}
- switch语句:用于多分支选择。例如:
switch (x) {
case 1:
// do something
break;
case 2:
// do something else
break;
default:
// do something if no case matches
break;
}
- break和continue语句:用于跳出循环或者继续执行循环。例如:
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // break out of the loop when i == 5
}
if (i % 2 == 0) {
continue; // skip even numbers
}
// do something with odd numbers before i == 5
}
- try-catch语句:用于捕获并处理异常。例如:
try {
// some code that may cause an exception
int result = 1 / 0;
} catch (ArithmeticException e) {
// do something when an exception is caught
int result = 0;
}
这些逻辑控制语句可以帮助我们编写复杂的程序,实现不同的流程控制逻辑,以及异常处理等功能。
8. 函数
在计算机编程中,函数是一段指定功能的可重用代码,在程序中多处使用的代码段可以通过函数进行封装,这也是面向对象编程中的一种编程思想。当程序需要执行函数中的代码时,只需要对该函数进行调用即可,使得程序的编写和组织更加方便和简洁。
函数通常包含以下几个要素:
-
函数名:函数的标识符,用于在程序中调用该函数。
-
参数列表:函数需要接收的输入参数,可以是零个或多个。参数列表可以为空,也可以包含多个参数,多个参数之间需要使用逗号进行分隔。
-
返回值类型:函数返回结果的数据类型,可以是任意数据类型,例如整型、布尔型、字符串型等等。
-
函数体:包含函数执行的代码。
函数的定义通常使用如下语法:
返回值类型 函数名(参数列表) {
函数体
return 返回值;
}
其中,返回值类型可以省略,如果函数不需要返回值则使用 void
进行表示;如果函数需要返回值,则返回值类型应该与返回结果的数据类型相符。
函数的调用使用函数名和对应的参数列表,例如:
函数名(参数列表);
在调用函数时,程序将会跳转到函数体中执行相应的代码,函数执行完毕后,可以返回结果给调用函数的位置,也可以不返回任何结果。
函数在计算机编程中的应用非常广泛,尤其是在大型项目中,函数可以使程序代码具有更高的可维护性、可扩展性和可重用性,提高开发效率,降低开发成本。
传值 & 传址
函数参数是指在调用函数时所传递给函数的值或变量。函数的参数可以分为两种,一种是值传递(传递的是变量的副本),另一种是引用传递(传递的是变量的地址)。
传值调用是指在调用函数时,实参的值被复制传递给形参,函数执行中对形参的修改不会影响实参的值。在这种情况下,函数可以改变形参的值,但实参的值不受影响。这种方法的优点是简单易懂,缺点是当参数数据量较大时,需要进行不必要的数据复制,浪费时间和空间。
例如:
#include <stdio.h>
void add(int x, int y) {
x = x + y;
printf("在函数内部:x = %d, y = %d\n", x, y);
}
int main() {
int a = 5, b = 10;
add(a, b);
printf("在函数外部:a = %d, b = %d\n", a, b);
return 0;
}
输出结果为:
在函数内部:x = 15, y = 10
在函数外部:a = 5, b = 10
从输出结果可以看出,函数内部对 x
的改变并没有对 a
产生影响。
传址调用是指在调用函数时,实参的地址被传递给形参,函数执行中对形参变量的修改会影响实参的值。这种方法的优点是减少了不必要的数据复制,节省时间和空间,缺点是实参的值可能会被误修改,程序安全性需要额外考虑。
例如:
#include <stdio.h>
void add(int *x, int y) {
*x = *x + y;
printf("在函数内部:*x = %d, y = %d\n", *x, y);
}
int main() {
int a = 5, b = 10;
add(&a, b);
printf("在函数外部:a = %d, b = %d\n", a, b);
return 0;
}
输出结果为:
在函数内部:*x = 15, y = 10
在函数外部:a = 15, b = 10
从输出结果可以看出,函数内部对 *x
的改变同时也改变了 a
的值。
总之,函数参数的传递方式需要根据实际情况选择,传值调用适用于参数数据量较小、不需要修改原始数据的情况;传址调用适用于参数数据量较大、需要修改原始数据的情况。
//printLoginUI() // 显示登录界面
// 现在在printLoginUI函数里, 获取了用户输入的用户名和密码
// 变量在他函数内部, login函数无法获取, 怎么办?
// 我们这个函数要用, 让那个函数有返回值? 把用户名和密码返回给我们?
// 函数只能有一个返回值, 不太行...
// 我们先试试 这样:
// 应为这个函数里要用用户名和密码, 所以, 我们在这个函数里定义变量
// 然后把变量传递到printLoginUI函数里, 等printLoginUI函数运行完是不是就OK了?
// 试试看
var username string
var password string
printLoginUI(&username, &password)
// 运行看看...
fmt.Printf("用户名是: %s, 密码是: %s\n", username, password)//调试信息, 以后删掉
// 发现问题, 通过参数传递过去的 login函数里的 username, password变量的值根本没有修改
// 原因是: 参数传递的时候传递的是变量的值, 我们现在这种写法,
// 在调用printLoginUI函数前, login函数里的 username, password 是空值
// 调用printLoginUI函数时 printLoginUI(username string, password string)
// 这两个参数变量是新的字符串变量, 并被赋值成空了, 在rintLoginUI里输入的字符都到了参数这两个变量里了
// 所以没有起作用, 要想起作用, 就需要把login函数里的这两个变量的"地址"传递给上面的函数
// 地址的值用指针表示, 修改printLoginUI函数
// 传递的时候, 把login函数里的这两个变量的地址传递进去, 那么在printLoginUI函数内部,
// 用户输入的值就存放到了ogin函数里的这两个变量里了
// 现在再试一下
9. 不要重复发明轮子 – 包
前人做好的轮子, 会打成"包" 发布出来, 有些包直接随着编译环境附带了, 有些则需要你下载, 放到你的工程里, 使用的时候"引入"$\longrightarrow$import 就能用了
Go语言中的包(Package)是指多个Go源文件的集合,通常在一个包中放置一组相关性很高的文件。
在Go语言中,每一个源文件都属于且只属于一个包;每个包都有一个名字和一个对应的导入路径;包名可以与所在的目录名不同,但是包名必须与导入路径的最后一个元素一致。
包的导入可以使用 import
关键字实现,例如:
import (
"fmt"
"strings"
)
在上述示例中,引入了两个内置包 fmt
和 strings
。
可以使用关键字 package
定义包名,例如:
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
在上述示例中,定义了 main
包, main
包是每个可执行程序所必须包含的一个入口包,其中 main
函数是程序的入口函数。
同一个包内的代码可以相互调用,不同包之间的代码调用需要使用包名进行限定,例如:
package main
import (
"fmt"
"mypackage" // 引入自定义包
)
func main() {
fmt.Println(mypackage.Add(1, 2))
}
其中,mypackage.Add(1, 2)
表示调用自定义包 mypackage
中的 Add
函数。在不同的包中,函数名和变量名具有作用域限定,以保证不同包中的同名函数或变量不会产生冲突。
总之,对于大型的Go应用程序来说,包是组织和管理代码的基本单元,具有高度的独立性、可重用性和可扩展性。合理地使用包可以提高代码的可读性、可维护性和可测试性。
10. 数据的集合
Go语言中的数据集合
Go语言中的数据集合是指一组有序或无序的数据元素的集合,根据数据的组织方式不同,可以分为以下几种类型:
- 数组(Array):同类型的有序元素的集合,定义时需要指定长度,数组长度不可变。数组元素可以通过下标来进行访问和修改。
var arr [10]int //定义一个长度为10的整型数组
arr[0] = 1 //给第一个元素赋值1
- 切片(Slice):动态大小的、可以增长和缩小的序列,由指向数组的指针、长度和容量三个部分构成。切片可以通过
make
函数创建,也可以从现有数组或切片中创建。
slice := make([]int, 3, 5) //创建一个长度为3,容量为5的整型切片
slice1 := []int{1, 2, 3} // 创建一个有3个元素的整型切片,并初始化
- 映射(Map):一组键值对的无序集合,通过键快速查找对应的值,键和值可以是任意类型。映射可以通过
make
函数创建,也可以使用字面量创建。
map1 := make(map[string]int) //创建一个字符串到整数的映射
map1["a"] = 1 //为映射设定键值对
- 集合(Set):一组无序的、不重复的数据元素集合,通常用于去重和数据统计。Go语言中没有原生的集合类型,但是可以使用切片结合map来实现一个简单的集合。
var s []int //定义一个整型切片
set := map[int]bool{} //定义一个用于存放唯一元素的字典
s = append(s, 1, 2, 3) //向切片添加元素
for _, v := range s {
set[v] = true //使用字典记录唯一元素
}
总之,Go语言中提供了多种数据集合类型,有序或无序、动态或静态、可变或不可变,用户可以根据需要选择合适的数据类型进行存储和操作。对于大型的应用程序来说,选择合适的数据类型能够提高程序的性能和可维护性。
Python语言的数据集合
Python语言的数据集合主要包括以下几种类型:
- 列表(List):用于存储一组有序的数据元素,列表的长度和内部元素都可以动态改变。
my_list = [1, 2, 3, 4] #定义列表
my_list.append(5) #向列表中添加元素
- 元组(Tuple):和列表类似,但元组的长度和内部元素不可变,通常用于存储不可变的数据。
my_tuple = (1, 2, 3, 4) #定义元组
- 字典(Dict):用于存储一组键值对的无序集合,键和值可以是任意类型,可以动态增加或删除键值对。
score = {'Tom': 90, 'Jerry': 80, 'Alice': 95} #定义字典
score['Jack'] = 85 #添加键值对
- 集合(Set):一组无序的、不重复的数据元素集合,通常用于去重和数据统计。
my_set = set([1, 2, 3, 4, 3, 2]) #创建集合
- 字符串(String):用于存储一组有序的字符元素。
my_str = 'abcdefg' #定义字符串
- 数组(Array):和列表类似,但数组中元素的类型必须相同,通常用于数值计算和科学计算。
import numpy as np
my_array = np.array([[1, 2, 3], [4, 5, 6]]) #定义二维数组
总之,Python语言提供了多种数据结构,用户可以根据需要选择合适的数据类型来存储和操作数据。这些数据结构拥有不同的特点和用途,比如集合可以用于去重和数据的统计,列表可以用于存储单一类型的数据和一些排序算法的实现,数组可以用于数值计算和科学计算等。对于大型的应用程序来说,选择合适的数据类型和数据结构能够提高程序的性能和可维护性。
Java语言的数据集合
Java语言的数据集合主要包括以下几种类型:
- 数组(Array):一组有序的数据元素,数组的长度不可变。
int[] myArray = new int[]{1, 2, 3, 4}; //定义数组
- 列表(List):用于存储一组有序的数据元素,列表的长度和内部元素都可以动态改变。
List<Integer> myList = new ArrayList<>(); //定义列表
myList.add(1); //向列表中添加元素
- 集合(Set):一组无序的、不重复的数据元素集合,通常用于去重和数据统计。
Set<Integer> mySet = new HashSet<>(); //定义集合
mySet.add(1); //向集合中添加元素
- 映射(Map):用于存储一组键值对的无序集合,键和值可以是任意类型,可以动态增加或删除键值对。
Map<String, Integer> myMap = new HashMap<>(); //定义映射
myMap.put("Tom", 90); //添加键值对
- 队列(Queue):用于存储一组有序的元素,支持添加和移除操作。
Queue<Integer> myQueue = new LinkedList<>(); //定义队列
myQueue.offer(1); //向队列中添加元素
myQueue.poll(); //从队列中移除元素
- 栈(Stack):一种先进后出(LIFO)的数据结构,支持添加和弹出操作。
Stack<Integer> myStack = new Stack<>(); //定义栈
myStack.push(1); //向栈中添加元素
myStack.pop(); //从栈中弹出元素
总之,Java语言提供了多种数据集合类型,用户可以根据需求选择合适的数据类型进行存储和操作。这些数据集合类型有着不同的特点和用途,比如集合可以用于去重和数据的统计,列表可以用于存储单一类型的数据和一些排序算法的实现,映射可以用于存储键值对等。通过合理地选择和使用数据集合类型,可以提高程序的性能和可维护性。
// 定义 再 赋值
var fruitsArr [2]string
fruitsArr[0] = "apple"
fruitsArr[1] = "orange"
fmt.Println(fruitsArr)
fmt.Println(fruitsArr[1])
// 定义 并 赋值
phonesArr := []string{"iphone", "mi", "huawei", "oppo"}
fmt.Println(phonesArr)
// 数组长度
length := len(phonesArr)
fmt.Println("phonesArr 长度=", length)
// 遍历数组
fmt.Println("遍历方法1 for循环len(ary)")
for i := 0; i < length; i++ {
fmt.Println(i, phonesArr[i])
}
fmt.Println("遍历方法2 用range, 不想要下标索引的话 i 用_代替 (_, item := range arr)")
for i, item := range phonesArr {
fmt.Println(i, item)
}
// 增加元素
phonesArr = append(phonesArr, "vivo")
fmt.Println("增加元素vivo以后的数组:", phonesArr)
// 数组切片 左包右不包
pArr1 := phonesArr[:1] // 从开始 切到 0
pArr2 := phonesArr[1:3] // 从下标1开始 切到 2
pArr3 := phonesArr[2:] // 从下标2开始 切到最后
fmt.Println("[:1] 从开始切到下标是0的数组:", pArr1)
fmt.Println("[1:3] 从下标1开始切到2的数组:", pArr2)
fmt.Println("[2:] 从下标2开始 切到最后的数组:", pArr3)
// 删除数组中的元素
which := 2
phonesArr = append(phonesArr[:which], phonesArr[which+1:]...)
fmt.Println("删除下标是which以后的数组:", phonesArr)
关于…语法糖
11. 数据的抽象
// 由于在用户使用的时候, 不同用户的菜单不同, 这就意味着权限不同,
// 用户的角色决定用户的权限, 因此 只用用户名和密码这两个数据不能达到目的
// 因此, 我们要给用户加一个"属性": 角色,
// 再建立一个数组? 变成 一个数组存放用户名, 一个存放密码, 再来一个存放 角色?
// 还是建立第二个map? key是用户名, value是角色?
// 可见, 这种横向的组织数据方式让我们的编程变得复杂, 且难以理解
// 所以 编程的时候要对模型进行抽象, 用抽象的思维去解决问题
// 把计算机要管理的内容, 抽象成现实世界中的"对象" 这就叫面向对象编程
// go语言对面向对象的支持很像c语言, 支持的不是很好, python还可以, 但python是个胶水语言, 对这方面不是很在意
// Java是个纯纯的面向对象编程语言, 因此在如果面向对象编程这块儿, 我会用Java举例子
// 现在, 我们用go开发的这个系统, 怎么对数据进行抽象的就是下面要讲的内容: 结构体
// 定义结构体: 用户结构
type User struct {
username string // 第一个属性 用户名 字符串类型
password string // 第二个属性 密码 字符串类型
role int // 第三个属性 角色 整数类型
}
使用方法
// 定义一个User 并付给他初值, 用我们的admin例
user := User{username: "admin", password: "123456", role: 9}
// 看里面的属性:值 是不是很像map, 对, key是属性名, value是:后面的
// 定义一个User, 后面再赋值
var user1 User
user1.username = "user1"
user1.password = "654321"
user1.role = 1 // 我们一共也没几个角色, 所以我们把超级管理员设为最大的9 , 普通用户设为1 这样应该够用
// 对于结构体的属性单独赋值, 用.取属性名 =赋值
// 面向对象是把属性和对应自己属性的相关方法放在一起进行抽象 抽象为: "类", 我们一会儿用Java来举例子
// 那么, go语言是否可以? 当然也是可以的, 就是定义函数, 然后把指向结构体的指针传给函数, 那这个函数就能操作结构体里面的属性了
// 这就是把数据和方法抽象到一个"类"里了, 其他语言都是这样的, python 用了 self 关键字, Java 用了this, js也是this
// 所以以后在遇到this,self这样的字眼的时候, 你就按照go这样的理解就好,指向自己的指针(没有指针的语言, 就是自己本身的引用)
// 做个例子, 改变密码:
func (pThis *User)changePassword(newstr string) bool {
if pThis.password == newstr {
return false
} else {
pThis.password = newstr
}
return true
}
定义函数的方法, 把这个函数和结构体关联在一起了, 这就是go的面向对象方法, 抽象
// 我们来实验一下, 改变admin的密码
changeResult := user.changePassword("123456")
fmt.Println("改变密码为123456:", changeResult)
changeResult = user.changePassword("654321")
fmt.Println("改变密码为654321:", changeResult)
// 运行一下看看结果
改变密码为123456: false 改变密码为654321: true
// 现在看这个User类型(结构体类型) 是不是代表了用户的抽象?
// 在整个编程之前, 把数据和对数据的操作都放在"类"里, 这样去编程,就是面向对象编程, 之后我们用Java进行更详细的讲解
现在我们把结构体讲完了, 就是这么简单, 当然结构体作为数据类, 它可不可以放在数组里? 可不可以放在 map里?
当然是可以的!
我们学习了结构体下一步, 这就确定了编程中非常重要的一件事: 数据结构
不管编什么程序, 数据结构都是非常非常重要的! 有人说编程就是数据结构+算法
细想一下, 古人诚不欺我!
go语言的接口
Go语言中的数据抽象是通过接口(Interface)实现的。接口定义了一组行为,只有实现了接口的类型才能够使用该接口。
在Go语言中,接口是一种类型,由一组方法签名定义。任何类型只要实现了接口中定义的所有方法,就被认为是该接口的实现类型,可以被赋予该接口类型的变量。
例如,下面是一个定义接口的示例:
type Shape interface {
area() float64
perimeter() float64
}
接口 Shape
定义了两个方法 area
和 perimeter
,所有满足该接口方法签名的类型都可以被称为 Shape
类型。例如:
type Rectangle struct {
width, height float64
}
// 实现Shape接口中的area方法
func (r Rectangle) area() float64 {
return r.width * r.height
}
// 实现Shape接口中的perimeter方法
func (r Rectangle) perimeter() float64 {
return 2 * (r.width + r.height)
}
在上述示例中,Rectangle
结构体实现了 Shape
接口所定义的两个方法 area
和 perimeter
,因此可以被赋予 Shape
类型的变量。
通过接口的实现,可以实现数据抽象和面向接口编程,提高代码的复用性和可维护性。比如,可以定义多个实现同一个接口的不同类型,然后在调用该接口的函数中使用这些实现类型的实例,从而实现对不同对象的处理。这种方式能够减少代码的重复性,简化程序的结构,提高程序的可扩展性。
python语言的数据抽象
在Python语言中,数据抽象是通过面向对象编程(Object-Oriented Programming)实现的。数据抽象是指一种编程技术,它可以将数据结构和数据操作隐藏起来,只暴露对外的接口,从而实现良好的封装性和抽象性。
在Python语言中,可以通过定义类和实现类的方法来实现数据抽象。类(class)定义了一种数据类型的结构,类的实例(instance) 是该类数据类型的具体表示。方法(method) 是类的函数,通过方法来定义该类的行为。
例如,下面是一个定义类和实现方法的示例
class Shape:
def area(self):
pass
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
在上述示例中,Shape
定义了两个方法 area
和 perimeter
,而 Rectangle
继承了 Shape
并实现了 area
和 perimeter
这两个方法。在 Rectangle
中, area
和 perimeter
就是该类的行为,通过方法实现了数据的抽象。
通过类的继承和方法的覆盖,我们可以根据需要定义不同的类及其方法,实现数据结构和数据操作的隐藏。将数据操作隐藏起来,只暴露对外的接口,可以保证数据的完整性和安全性。同时,在需要修改数据操作时,也可以更容易地完成修改而不会影响其他地方的调用,从而提高了可维护性和可扩展性。
Java语言的数据抽象
在Java语言中,数据抽象是通过面向对象编程(Object-Oriented Programming, OOP)实现的。数据抽象是一种编程技术,它可以将数据结构和数据操作隐藏起来,只暴露对外的接口,从而实现良好的封装性和抽象性。
在Java语言中,可以通过定义类和实现类的方法来实现数据抽象。类(Class)定义了一种数据类型的结构,而类的实例(Instance)是该类数据类型的具体表示。方法(Method)是类的函数,描述了该类的行为。
例如,下面是一个定义类和实现方法的示例:
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}
在上述示例中,定义了一个 Shape
类和一个 Rectangle
类。Shape
类定义了两个抽象方法 area
和 perimeter
,而 Rectangle
类继承了 Shape
类并实现了 area
和 perimeter
方法。在 Rectangle
类中,area
和 perimeter
就是该类的行为,通过方法实现了数据的抽象。
通过类的继承和方法的覆盖,我们可以根据需要定义不同的类及其方法,实现数据结构和数据操作的隐藏。将数据操作隐藏起来,只暴露对外的接口,可以保证数据的完整性和安全性。同时,在需要修改数据操作时,也可以更容易地完成修改而不会影响其他地方的调用,从而提高了可维护性和可扩展性。
文件存储
文件类型
用户的信息不能就这样写代码里的, 因为代码编译后就固定了, 每次运行程序都是同样的值, 所以应该保存在程序外部,属于程序的数据部分,现阶段, 我们的计算机只能用**“文件”**来保存数据;
前面讲过, 文件有2种, 文本文件和二进制文件
文本文件说白了也是二进制文件,但结构非常简单就是里面是按照字符的二进制一个一个保存的, 由此可以知道文本文件里存储的全都是字符 , 一定要记住这一点!
那么二进制文件呢? 里面存储的就是内存中的那些算盘…, 举个例子:
我们创建一个文本文件, 用文本编辑器写一个1, 保存以后, 这个文件实际存的是什么? 对, 是它的ASCII码 : 00110001 转换成16进制是0x31
我们创建一个int 变量, 赋值为1 用程序保存成二进制文件, 这个文件实际存储的是什么? 这个要和操作系统相关了, 但是简单说来, 应该是:00000001 我们现在是64位机器, 所以1在文件里应该是 00000000 00000000 00000000 00000001 对吧
毕竟现在编程用二进制保存文件的场合不是很多, Unix哲学说过的吧, 文本才是好的接口哦, 就算一定用二进制保存内容, 这也是数据库程序干的事了, 所以我们暂时不学习二进制文件的读写, 在我们的程序里, 就只学文本文件的读写, 就是把我们的数据(例如用户) 转换成字符串写入到文本文件里, 这个操作在行业里被称为 “序列化(serialization)” , 从文本文件中读出字符串,再转化为我们的数据, 这叫反序列化(deserialization)
可见对字符串的操作是非常重要的非常有用的, 所以我们先学习一下"字符串"相关的操作, 前面说过任何一门编程语言里,字符串的操作都是非常重要的, 在C语言里, 只有字符数组, 我们来看看go语言字符串的相关操作
编写测试代码
我们在工程里创建一个teststr.go文件, main 包里, 实验的方法都写在这里, 修改main函数调用一下测试方法, 通过实践来学习一下字符串的操作
teststr.go
package main
func testStr() {
}
main.go
func main() {
testStr()
return
}
字符串相关操作
1. 字符串长度
我们定义2个字符串变量, 再分别输出用len函数求出的他们的长度:
func testStr() {
s1 := "SSMS"
s2 := "学生成绩管理系统"
fmt.Println("SSMS长度是:", len(s1))
fmt.Printf("%s的长度是:%d\n", s2, len(s2))
}
运行结果:
SSMS长度是: 4
学生成绩管理系统的长度是:24
关于ASCII 码 和UTF-8
len函数的返回值的类型为 int,表示字符串的 ASCII 字符个数或字节长度。
“SSMS"是ASCII码, 所以结果是4
“学生成绩管理系统"是中文, Go 语言的中文字符串都以 UTF-8 格式保存,每个中文占用 3 个字节,因此使用 len() 获得8个中文文字对应的 24 个字节。
如果想知道我们意思里真正的长度呢? 需要用unicode/utf8包下的RuneCountInString()函数:
package main import ( "fmt" "unicode/utf8" ) func testStr() { s1 := "SSMS" s2 := "学生成绩管理系统" fmt.Println("SSMS长度是:", len(s1)) fmt.Printf("%s的长度是:%d\n", s2, len(s2)) fmt.Printf("%s的长度是:%d\n", s2, utf8.RuneCountInString(s2)) }
结果输出:
SSMS长度是: 4 学生成绩管理系统的长度是:24 学生成绩管理系统的长度是:8
2. 遍历字符串中的字符
我们用for循环遍历s1的每一个字符, 并把每个字符ASCII码显示出来
for i:=0; i < len(s1); i++ {
fmt.Printf("第%d个字符是%c ASCII码是%d 二进制是%b \n", i, s1[i], s1[i], s1[i])
}
结果输出:
第0个字符是S ASCII码是83 二进制是1010011
第1个字符是S ASCII码是83 二进制是1010011
第2个字符是M ASCII码是77 二进制是1001101
第3个字符是S ASCII码是83 二进制是1010011
我们用同样的方式对s2呢? 试试看
for i:=0; i < len(s2); i++ {
fmt.Printf("s2第%d个字符是%c ASCII码是%d 二进制是%b \n", i, s2[i], s2[i], s2[i])
}
结果输出:
这就是乱码, 针对我们非英语的语系, 明显我们遍历的不是要遍历ASCII字符, 这时候我们就要用range函数来遍历
for i, c := range s2{
fmt.Printf("s2第%d个字 是%c 整数数值是%d 二进制是%024b \n", i, c, c, c)
}
结果输出:
s2第0个字 是学 整数数值是23398 二进制是000000000101101101100110
s2第3个字 是生 整数数值是29983 二进制是000000000111010100011111
s2第6个字 是成 整数数值是25104 二进制是000000000110001000010000
s2第9个字 是绩 整数数值是32489 二进制是000000000111111011101001
s2第12个字 是管 整数数值是31649 二进制是000000000111101110100001
s2第15个字 是理 整数数值是29702 二进制是000000000111010000000110
s2第18个字 是系 整数数值是31995 二进制是000000000111110011111011
s2第21个字 是统 整数数值是32479 二进制是000000000111111011011111
那我们对s1也做同样的操作呢?
for i, c := range s1{
fmt.Printf("s1第%d个字 是%c 整数数值是%d 二进制是%b \n", i, c, c, c)
}
结果:
s1第0个字 是S 整数数值是83 二进制是1010011
s1第1个字 是S 整数数值是83 二进制是1010011
s1第2个字 是M 整数数值是77 二进制是1001101
s1第3个字 是S 整数数值是83 二进制是1010011
可以看出, 完全就是ASCII码, 所以, 可以得出:中文字符和英文字符都可以用range变量, 中文字符是UTF-8,英文字符是ASCII
看不见的字符也是有数值的:
fmt.Printf("换行符的整数数值是%d 二进制是%08b 十六进制是:%02X\n", '\n', '\n', '\n')
结果:
换行符的整数数值是10 二进制是00001010 十六进制是:0A
3. 字符串查找
前面我们讲包的时候, 用过strings包的那个重复多少次函数记得吧, 是的, 基本每个语言都有这么个包专门处理字符串的, 用包里的函数就可以方便地进行操作
// 用strings.Index 查找
fmt.Println("SSMS 中的M的位置是:", strings.Index(s1, "M"))
fmt.Printf("%s 中的成的位置是 %d \n", s2, strings.Index(s2, "成"))
结果:
SSMS 中的M的位置是: 2
学生成绩管理系统 中的成的位置是 6
反向查找呢? 一般的语言都是LastIndex…, 还有strings.Count()和strings.Contains()等函数, 什么意思猜都能猜到…用IDE的好处, 打个点看有什么函数, 猜意思试一下
4. 字符串截取
从上面例子可以看出, 字符串就是字符数组, 数组的切片还记得不?
p := strings.Index(s1, "M")
fmt.Printf("s1从M p=%d 的位置拆分为[%s]和[%s] \n", p, s1[:p], s1[p:])
p = strings.Index(s2, "成")
fmt.Printf("s2从成 p=%d 的位置拆分为[%s]和[%s] \n", p, s2[:p], s2[p:])
结果:
s1从M p=2 的位置拆分为[SS]和[MS]
s2从成 p=6 的位置拆分为[学生]和[成绩管理系统]
5. 字符串拼接
简单拼接:
s3 := s1 + s2
fmt.Println("用+号拼接: ", s3)
结果:
用+号拼接: SSMS学生成绩管理系统
不在乎效率的时候, 可以随便使用,方便, 由于要频繁开辟内存, 释放内存, 所以在注重效率的场合例如网络编程, 需要用bytes.Buffer:
var buffer bytes.Buffer
buffer.WriteString(s1)
buffer.WriteString(s2)
fmt.Println("把buffer转换成string:", buffer.String())
可以看出,这是bytes包的, 一个字节一个字节操作的, 所以也适合网络编程, 以后我们会讲, 结果:
把buffer转换成string: SSMS学生成绩管理系统
我们想把数值拼到字符串里呢? 例如:
我们想定义一个前面说到的User结构体:
user1 := user.User{username:"admin", password:"123456", role:9}
这时候, IDE会提示错误, 错误的大意是没有导出的字段(field) username 什么的, 为什么, 因为我们User结构体定义在user包里, 之前使用都是在user包里(Velidate函数)中, 所以有权限访问username等, 现在我们这个测试程序是在main包里, 包外是无法访问的, 想在user包外使用, 要像函数一样, 将首字母大写, 因此要修改User结构体:
// 定义结构体: 用户结构
type User struct {
Username string // 第一个属性 用户名 字符串类型
Password string // 第二个属性 密码 字符串类型
Role int // 第三个属性 角色 整数类型
}
func (pThis *User)ChangePassword(newstr string) bool {
if pThis.Password == newstr {
return false
} else {
pThis.Password = newstr
}
return true
}
到这儿, 这个User结构体才算基本完成
大写好像Java里的public修饰符, 小写好比private…
我们再回去把这个测试的user1改一下:
user1 := user.User{
Username: "admin",
Password: "123456",
Role: 9,
}
strUser := user1.Username + user1.Password
fmt.Println(strUser)
运行的结果:
admin123456
我们想把Role也放进去呢:
strUser := user1.Username + user1.Password + user1.Role
// 会报下面的错误:
// invalid operation: user1.Username + user1.Password + user1.Role (mismatched types string and int)
这时候就需要把数值9转换成字符串"9"的函数了, 同样字符串"9"想转换成数值9也有对应的函数(strconv.Atoi(“9”)), 他们都在strconv包里(别的语言也有, Java能特殊一点, 到时候我们再讲):
strUser := user1.Username + user1.Password + strconv.Itoa(user1.Role)
fmt.Println(strUser)
结果:
admin1234569
6. 格式化字符串
接着刚才的例子, admin1234569我们分不清哪个是用户名,哪个是密码, 怎么办? 我们把每个字段中间加个空格字符是不是好很多?
strUser = user1.Username + " " + user1.Password + " " + strconv.Itoa(user1.Role)
这样的代码是不是很Ugly? 想想我们之前在控制台输出字符时候用的fmt包, 它在控制台输出字符, 本质是不是也是操作字符?所以, 它也有这个功能–fmt.Sprintf, 而且非常强大, 我们来看看:
//fmt.Sprintf("用户名 密码 角色", 用户名, 密码, 角色)
strUser = fmt.Sprintf("%s %s %d", user1.Username, user1.Password, user1.Role)
fmt.Println(strUser)
结果:
admin 123456 9
是不是很方便? 想想Sprintf是怎么实现的呢? 如果让你来写这个Sprintf函数, 你会怎么写? 试试看?
再思考一下, 为什么很多系统不许用空格做用户名, 或者密码? 现在这个程序在用户输入之后是否要做些检查?
7. 字符串拆分
上面我们把user1变成了一个字符串, 现在我们想把这个字符串再变回我们的数据结构, 怎么办?这就需要对字符串进行拆分:
go语言strings包特有的:
arr := strings.Fields(strUser)
fmt.Println(arr)
结果:
[admin 123456 9]
可以看出, 返回值arr是一个数组, 第一个元素是:admin…
大多数语言都有的, 且很通用的Split函数:
arr = strings.Split(strUser, " ")
fmt.Println(arr)
结果:
[admin 123456 9]
8. 字符串转换成数值
var user2 user.User
user2.Username = arr[0]
user2.Password = arr[1]
user2.Role, _ = strconv.Atoi(arr[2])
fmt.Println(user2)
结果:
{admin 123456 9}
到此, 字符串的学习差不多了, 学到最后, 是不是感觉, 我们的用户信息,是不是可以写到文件里? 登录校验的时候是不是可以到文件里去读取用户进行用户名和密码的校验?
面向对象编程
这里扩展讲一下面向对象的编程
看上面我们在字符串实验函数里, 定义了一个User型变量user2, 然后把字符串 “admin 123456 9” Split之后分别为user2的属性赋值, 得到了一个user2的具体的值, 这就是典型的面向过程编程, 这个和User相关的操作是在User外部进行的, 你想一下很明显, 这个操作是和User相关的(都是操作User本身的数据), 如果把这个操作的过程, 放到User类型内部呢? 是不是更加合理?
就是面向对象编程的重要思想: 把相关数据以及对数据的操作抽象在一起
前面讲过, 在go语言中用结构体和带自己指针的函数这样的方式表达"类"的概念, 因此我们为上面的操作定义一个类的函数(在面向对象编程中这个函数叫做方法)FromString, 现在到user包里去实现它:
func (pThis *User)FromString(strUser string) bool { arr := strings.Fields(strUser) if len(arr) != 3 { return false } role, err := strconv.Atoi(arr[2]) if err != nil { return false } else { pThis.Username = arr[0] pThis.Password = arr[1] pThis.Role = role return true } }
实验一下:
strUser3 := "admin2 121212 9" var user3 user.User user3.FromString(strUser3) fmt.Println(user3)
结果:
{admin2 121212 9}
我们把一个固定格式的字符串变成User对象本身的函数抽象到User类型的内部了, 那么, 把User类型对象转换成固定格式的字符串呢? 是不是也应该放到User类型里呢? 实现一下ToString方法!
func (pThis *User)ToString() string { return fmt.Sprintf("%s %s %d", pThis.Username, pThis.Password, pThis.Role) }
实验一下:
fmt.Println(user3.ToString())
结果:
admin2 121212 9
程序分层
现在我们的代码越来越多, 最好对代码进行一下划分,把他们分一下层(同样作用的放到一起), 这样找起来容易, 阅读起来也比较方面, 别忘了我们现在作为"架构师"代码是给别人"打个样"的, 不能啥都堆到一起…
现在开始做这件事, 看看我们的user包(user目录下), 现在就一个user.go文件, 里面除了User类型和他的方法的定义, 还有个Velidate函数(校验用户名密码) 想一下, Velidate可否也变成User的方法吗? …
答案是不能, 因为校验不是对User的属性数据进行操作的, 这个函数明显是我们的程序提供的一个"服务”, 意思是给我一个user, 程序来判断用户是不是我这个程序的用户, 密码是不是这个用户的密码… 简单说,这就叫"服务”, 将来我们把类似的功能函数放在一起, 所以我们在user目录下创建一个service.go文件, 并把Velidate函数剪切过来:
package user
import "fmt"
// 验证用户名密码
func Validate(username string, password string) bool{...}
我们继续完善user包, 现在user.go里面只有User类型和他自己的操作, 它是什么? – 对, 我们的"数据模型" 就是我们前面说过的M, 我们现在都是直接在代码里写用户名密码…正式程序不会这么做的, 这些数据都是要"落盘"(存储到磁盘上)的, 既然有存储,就需要读取, 对数据的存储和读取这部分程序被称为**“DAO”** (Data Access Object) 这个叫法是从Java程序来的,因为Java是面向对象的语言, 所以不论是什么都是对象, 现在慢慢的不这么叫了, 分层的时候一般把DAO这块儿叫 repository 但, 面试或平常沟通一般还是说"道儿"大家都明白什么意思, 我们现在学习go语言,沿用这个叫法后面目录划分的时候用repository
等我们做完这部分以后, 思考一下Service和Dao有什么区别
我们在user目录下创建dao.go文件(当然也是user包的内容), 并声明一个文件路径常量:
package user
// 用户信息存储文件
const USER_FILE_PATH_NAME = "./data/users.txt"
这个目录的意思是什么? 它在哪个位置?
我们将会把用户信息保存在"./data/users.txt"这个文件里, 现在的目标是完成DAO部分的CRUD,
C – create 在users.txt里创建一个新的用户
R – Research 在user.txt里查询某个用户
U – Update 更新users.txt里某个用户的信息
D – Delete 删除users.txt里的某个用户的信息
接下来我们一个一个实现, 先实现创建用户(用户是这个程序必备的部分, 因此,我们先把我们用代码创建的用户保存到文件中, 对于文件来说是create):
文件的创建
我们先来创建用户数据文件(USER_FILE_PATH_NAME)
package user
import (
"fmt"
"os"
"path/filepath"
)
// 用户信息存储文件
const USER_FILE_PATH_NAME = "./data/users.txt"
// 创建用户数据文件
func CreateUserFile() bool {
//TODO
return true
}
go语言对文件的操作函数在 os io/ioutil 和 bufio里面,用的时候查API就行
-
获取程序工作目录
// 获得程序的当前目录 appwd, err := os.Getwd() if err != nil { fmt.Println("获取SSMS工作目录错误:", err) return false } fmt.Println("SSMS程序工作目录:", appwd)
-
获取用户数据文件的绝对路径
// 用户数据文件全路径 pathname, err := filepath.Abs(USER_FILE_PATH_NAME) if err != nil { fmt.Println("获取用户数据文件的绝对路径错误:", err) return false } path, name := filepath.Split(pathname) fmt.Println("SSMS程序用户数据文件的绝对路径:" + path + " 文件名:" + name)
-
创建用户数据文件的文件夹
// 创建用户数据文件的文件夹 err = os.MkdirAll(path, os.FileMode(0777)) if err != nil { fmt.Println("创建用户数据文件夹错误:", err) return false }
-
判断用户数据文件是否存在, 如果不存在则创建文件, 否则不做操作
// 判断文件是否存在 fInfo, err := os.Stat(pathname) if err != nil {// 文件不存在 // 创建文件 _, err = os.Create(pathname) if err != nil { fmt.Printf("创建用户数据文件%s失败:%s \n", pathname, err) } else { fmt.Println("创建用户数据文件成功! ", pathname) } } else { // 文件存在 if fInfo.IsDir() { // 是个文件夹 fmt.Println("用户数据文件被同名文件夹占用, 请联系管理员!") return false } fmt.Println("创建用户数据文件存在, 不做操作! ", pathname) }
第一次运行结果:
SSMS程序工作目录: /Users/zhouleqing/Documents/_Lession/source/go/ssms
SSMS程序用户数据文件的绝对路径:/Users/zhouleqing/Documents/_Lession/source/go/ssms/data/ 文件名:users.txt
创建用户数据文件成功! /Users/zhouleqing/Documents/_Lession/source/go/ssms/data/users.txt
第二次运行结果, 可见当文件已经存在的时候我们没做任何操作:
SSMS程序工作目录: /Users/zhouleqing/Documents/_Lession/source/go/ssms
SSMS程序用户数据文件的绝对路径:/Users/zhouleqing/Documents/_Lession/source/go/ssms/data/ 文件名:users.txt
创建用户数据文件存在, 不做操作! /Users/zhouleqing/Documents/_Lession/source/go/ssms/data/users.txt
到此,创建用户数据文件这个功能就完成了, 这部分属于程序初始化调用的方法, 因此我们在main.go创建一个init函数, main函数执行初期调用它:
// 程序初始化
func initData() bool {
// 初始化创建用户数据文件
ok := user.CreateUserFile()
if !ok {
return false
}
// TODO: 写入初始用户
return true
}
// main函数, 程序入口函数
func main() {
if !initData() {
fmt.Println("程序初始化错误! 结束运行")
return
}
return
}
如果是系统第一次运行, 我们希望把默认的管理员用户写入到用户数据文件中, 我们这样做:
文本文件的写入
在initData的TODO位置, 先固定写死一个admin用户, 调用DAO中的CreateUser函数, 把这个用户写入到用户数据文件中:
// 创建默认管理员用户
admin := user.User{Username: "admin", Password: "123456", Role: 9}
// 将默认管理员用户写入到用户数据文件中
ok = user.CreateUser(admin)
此时是编译不过的, 因为我们还没写CreateUser这个函数, 打开DAO.go 创建这个函数:
// 在数据文件中添加用户
func CreateUser(user User) bool {
// 用户数据文件全路径
pathname, err := filepath.Abs(USER_FILE_PATH_NAME)
if err != nil {
fmt.Println("获取用户数据文件的绝对路径错误:", err)
return false
}
// 以写的方式打开文件
file, err := os.OpenFile(pathname, os.O_WRONLY, os.FileMode(0666))
if err != nil {
fmt.Println("打开用户数据文件失败!", err)
return false
}
line := user.ToString() + "\n"
writer := bufio.NewWriter(file)
writer.WriteString(line)
writer.Flush()
file.Close()
return true
}
现在我们运行程序, 可以看到,users.txt里面多了一行
admin 123456 9
我们手动修改这个文本文件, 再次运行发现修改后的数据不见了, 因为我们写的CreateUser函数每次都从开头写, 所以这一行总是一样的值, 我们修改一下, OpenFile函数的参数加上os.O_APPEND:
file, err := os.OpenFile(pathname, os.O_WRONLY | os.O_APPEND, os.FileMode(0666))
我们再次运行发现变成2行了:
admin 123456 9
admin 123456 9
运行3次就会有3行, 这显然不对, 正确的逻辑应该是如果数据文件users.txt中没有admin, 才追加这个默认的admin用户, 否则不做操作, 要实现这个我们需要先完成DAO中的R–查询数据的操作, 逻辑是添加admin用户之前调用ResearchUser函数,参数是admin, ResearchUser函数打开用户数据文件users.txt, 逐行寻找用户名是admin的用户, 如果有,返回admin, 没有则返回nil, 这个函数需要用到文件读取API下面我们开始:
文本文件的读取
修改initData函数:
const uname = "admin"
const upass = "123456"
const urole = 9
// 查找用户名是admin的用户
pAdmin := user.ResearchUser(uname)
if pAdmin == nil {
// 将默认管理员用户写入到用户数据文件中
ok = user.CreateUser(user.User{Username: uname, Password: upass, Role: urole})
}
在Dao中实现ResearchUser函数:
// 通过用户名查询用户
func ResearchUser(uname string) *User {
// 用户数据文件全路径
pathname, err := filepath.Abs(USER_FILE_PATH_NAME)
if err != nil {
fmt.Println("获取用户数据文件的绝对路径错误:", err)
return nil
}
// 打开文件
file, err := os.OpenFile(pathname, os.O_RDONLY, os.FileMode(0666))
if err != nil {
fmt.Println("打开用户数据文件失败:", err)
return nil
}
// 创建读取缓冲区
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n') // 读到一个换行就结束
if err == io.EOF { // 如果读到文件末尾, 就结束
break
}
var user User
if user.FromString(line) { // 将字符串转换成user类型成功
if user.Username == uname {
file.Close() // 关闭文件
return &user
}
}
}
file.Close() // 关闭文件
return nil
}
我们先删除data目录和下面的文件, 运行一下程序, 可以看见创建了users.txt, 打开文件发现里面只有一个admin, 再运行一次, 发现没有变化, 这时我们修改密码为222222, 再运行一次程序, 发现没有变化, 达到了我们的目的; 现在在看看查询函数,可以看到我们写了2次file.Close, 这个很容易忘掉, 会造成内存泄露, 所以现代编程语言都有一种处理方式, go语言用defer , python 用with, Java用try finally让类似的操作变得简单
我们修改一下:
// 通过用户名查询用户
func ResearchUser(uname string) *User {
// 用户数据文件全路径
pathname, err := filepath.Abs(USER_FILE_PATH_NAME)
if err != nil {
fmt.Println("获取用户数据文件的绝对路径错误:", err)
return nil
}
// 打开文件
file, err := os.OpenFile(pathname, os.O_RDONLY, os.FileMode(0666))
if err != nil {
fmt.Println("打开用户数据文件失败:", err)
return nil
}
defer file.Close() // defer延迟语句, 后面的语句在函数退出时调用, 达到用完关闭的效果
// 创建读取缓冲区
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n') // 读到一个换行就结束
if err == io.EOF { // 如果读到文件末尾, 就结束
break
}
var user User
if user.FromString(line) { // 将字符串转换成user类型成功
if user.Username == uname {
return &user
}
}
}
return nil
}
完善程序架构及用户校验过程
在有了上面完成的数据访问层函数以后, 我们就可以完善我们用来用户校验的Service层函数: Velidata, 前面我们用过流程图, 下面我们用另外一种软件设计的图形–时序图, 来看看我们的登录校验流程:
sequenceDiagram 用户->>main : 运行 main->>Controller: 执行登录流程login Controller->>UI : 显示loginUI UI-->>用户 : 提示用户输入用户名密码 用户->>UI : 输入用户名密码 UI->>Controller : User数据 Controller->>Service : Velidate(校验) Service->>DAO : ResearchUser(查询) DAO-->>Service : 相同用户名的用户 Service->>Service : 校验 Service-->>Controller: 校验结果 alt 校验通过 Controller-->>main : 返回用户及权限 main->>UI : 显示mainMenu UI-->>用户 : 提示用户选择功能 else 校验失败 Controller-->>UI : 显示错误信息 UI-->>用户 : 提示用户再试或退出 end
下面用流程图画一下流程(用做比较):
st=>start: 开始
o1=>operation: 参数传入用输入的用户名/密码做成的User: pUser
o2=>operation: 打开./data/users.txt 文件
ot=>operation: 返回成功(true)
of1=>operation: 返回失败(false)
of2=>operation: 返回失败(false)
c1=>condition: 有IO错误?
oline=>operation: 读取一行的字符串
c2=>condition: 文件的末尾?
o3=>operation: 字符串转换成User结构
c4=>condition: 转换成功?
c5=>condition: 用户相同?
c6=>condition: 密码相同?
o4=>operation: 用文件中的Role给pUser赋值
ed1=>end: 关闭文件 结束
ed2=>end: 关闭文件 结束
ed3=>end: 关闭文件 结束
st->o1->o2->c1
c1(yes)->of1
c1(no)->oline->c2
c2(yes)->of2
c2(no)->o3->c4
c4(yes)->c5
c4(no)->oline
c5(no)->oline
c5(yes)->c6
c6(no)->oline
c6(yes)->o4->ot->ed1
of1->ed2
of2->ed3
很明显, 时序图在事件驱动的时候表现得非常好, 更符合我们在架构设计的时候对程序架构的把控, 流程图更注重细节, 对某个具体函数适合用流程图.
工程目录结构
现在我们重构一下我们的程序, 把层划分得更合理,同时把登录校验做完:
我们新建几个文件夹:
-
model
数据模型都放到这个包下
-
view
用户界面都放在这个包下
-
controller
控制逻辑都放在这个包下
-
service
业务服务代码放在这个包下
-
repository
数据访问放在这个包里
我们把之前的代码梳理归纳在这些包里:
model文件夹下, 新建user.go, 把原来user包下user.go的内容剪切(不包括第一行package)到model下的user.go里
repository文件夹下, 新建config.go , 把原来user/dao.go里的下面这句移到config.go里:
package repository
// 用户信息存储文件
const USER_FILE_PATH_NAME = "./data/users.txt"
再在repository文件夹下新建userDao.go文件, 把user/dao.go里的其他内容剪切过来, 这时你发现userDao.go报错, 原因是原来User类型结构体(现在应该叫"类"了)在同一个包下, 现在被我们移动到了model包下了, 所以需要加上导入:
import "ssms/model"
然后把代码中User定义都换成model.User, 完成之后的userDao.go文件内容:
package repository
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
)
import "ssms/model"
// 通过用户名查询用户
func ResearchUser(uname string) *model.User {
// 用户数据文件全路径
pathname, err := filepath.Abs(USER_FILE_PATH_NAME)
if err != nil {
fmt.Println("获取用户数据文件的绝对路径错误:", err)
return nil
}
// 打开文件
file, err := os.OpenFile(pathname, os.O_RDONLY, os.FileMode(0666))
if err != nil {
fmt.Println("打开用户数据文件失败:", err)
return nil
}
defer file.Close() // defer延迟语句, 后面的语句在函数退出时调用, 达到用完关闭的效果
// 创建读取缓冲区
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n') // 读到一个换行就结束
if err == io.EOF { // 如果读到文件末尾, 就结束
break
}
var user model.User
if user.FromString(line) { // 将字符串转换成user类型成功
if user.Username == uname {
return &user
}
}
}
return nil
}
// 在数据文件中添加用户
func CreateUser(user model.User) bool {
// 用户数据文件全路径
pathname, err := filepath.Abs(USER_FILE_PATH_NAME)
if err != nil {
fmt.Println("获取用户数据文件的绝对路径错误:", err)
return false
}
// 以写的方式打开文件
file, err := os.OpenFile(pathname, os.O_WRONLY | os.O_APPEND, os.FileMode(0666))
if err != nil {
fmt.Println("打开用户数据文件失败!", err)
return false
}
line := user.ToString() + "\n"
writer := bufio.NewWriter(file)
writer.WriteString(line)
writer.Flush()
file.Close()
return true
}
// 创建用户数据文件
func CreateUserFile() bool {
// 获得程序的当前目录
appwd, err := os.Getwd()
if err != nil {
fmt.Println("获取SSMS工作目录错误:", err)
return false
}
fmt.Println("SSMS程序工作目录:", appwd)
// 用户数据文件全路径
pathname, err := filepath.Abs(USER_FILE_PATH_NAME)
if err != nil {
fmt.Println("获取用户数据文件的绝对路径错误:", err)
return false
}
path, name := filepath.Split(pathname)
fmt.Println("SSMS程序用户数据文件的绝对路径:" + path + " 文件名:" + name)
// 创建用户数据文件的文件夹
err = os.MkdirAll(path, os.FileMode(0777))
if err != nil {
fmt.Println("创建用户数据文件夹错误:", err)
return false
}
// 判断文件是否存在
fInfo, err := os.Stat(pathname)
if err != nil {// 文件不存在
// 创建文件
_, err = os.Create(pathname)
if err != nil {
fmt.Printf("创建用户数据文件%s失败:%s \n", pathname, err)
} else {
fmt.Println("创建用户数据文件成功! ", pathname)
}
} else { // 文件存在
if fInfo.IsDir() { // 是个文件夹
fmt.Println("用户数据文件被同名文件夹占用, 请联系管理员!")
return false
}
fmt.Println("创建用户数据文件存在, 不做操作! ", pathname)
}
return true
}
现在重新看一下上面ResearchUser函数的代码, 返回的user指针指向的值(变量的内存空间)是在函数内部创建的, 我们用指针的目的就是为了当没有查找到用户的时候, 返回一个nil, 这种函数内部创建变量, 把地址给外部用的方式非常不好, 变量最好是哪里创建哪里销毁, 我们之前为了学习临时写成这样, 很早以前, 函数只能有一个返回值, 想返回多个值(例如上面这个函数, 我们想返回查询的User, 和差没查到, 就需要再构建一个数据类型, 加入是UserResult结构, 有2个成员: User和bool, Java语言里所有的都是对象, 对象可以是Null, Null可以表达这个意思, 我们现在的做法就是想用指针的nil来表达这个意思), go语言函数是可以有多个返回值的, python也可以(元组), 现在把这个函数完善一下:
// 通过用户名查询用户 func ResearchUser(uname string) (model.User, int) { var user model.User // 用户数据文件全路径 pathname, err := filepath.Abs(USER_FILE_PATH_NAME) if err != nil { fmt.Println("获取用户数据文件的绝对路径错误:", err) return user, 0 } // 打开文件 file, err := os.OpenFile(pathname, os.O_RDONLY, os.FileMode(0666)) if err != nil { fmt.Println("打开用户数据文件失败:", err) return user, 0 } defer file.Close() // defer延迟语句, 后面的语句在函数退出时调用, 达到用完关闭的效果 // 创建读取缓冲区 reader := bufio.NewReader(file) for { line, err := reader.ReadString('\n') // 读到一个换行就结束 if err == io.EOF { // 如果读到文件末尾, 就结束 break } var user model.User if user.FromString(line) { // 将字符串转换成user类型成功 if user.Username == uname { return user, 1 } } } return user, 0 }
在service包下创建userService.go文件, 用来写和user相关也业务服务, 我们先把校验写好, 同时再添加一个GetUser函数把DAO层的ResearchUser隔离起来(这种方法在以后的编程里比较重要, 现在建立这个意识就好):
package service
import (
"ssms/model"
"ssms/repository"
)
// User相关的业务服务
// 登录校验
func LoginValidate(loginUser model.User) bool {
// 用输入的用户名到文件里查询对应用户名的数据
user, count := repository.ResearchUser(loginUser.Username)
// 如果没有查询到结果
if count <= 0 {
return false
}
// 现在是能够查询到用户
// 开始判断密码是否正确
if loginUser.Password == user.Password {
return true
}
return false
}
// 通过用户名获取一个用户
func GetUser(username string) (model.User, int) {
return repository.ResearchUser(username)
}
在view包下创建界面相关的代码, 新建common.go, 把各个画面UI共通的部分写在这里:
package view
import (
"fmt"
"strings"
)
const SYS_NAME = "学生成绩管理系统"
const WIDTH int = 80
// 循环输出80个 c
// 这个函数以后轮留着给做表格用, 外面想用就需要: 首字母大写
func PrintLine(c string) {
fmt.Println(strings.Repeat(c, WIDTH) + "\n")
}
// 在控制台输出系统标题
func PrintSysTitle(name string) {
strLine := strings.Repeat("*", WIDTH)
fmt.Printf("%s\n>> %s - %s\n%s\n", strLine, SYS_NAME, name, strLine)
//fmt.Printf("#{strLine}\n>> #{SYS_NAME} - #{name}\n#{strLine}\n")
}
创建loginView.go文件, login相关的用户UI都写在这个文件里, 我们写2个函数
package view
import (
"fmt"
"ssms/model"
)
const LOGIN_TITLE = "用户登录"
// 显示登录用户界面函数, 返回User结构体, 存储的是用户输入的用户名和密码
func DisplayLoginUI() model.User {
PrintSysTitle(LOGIN_TITLE)
var user model.User
// 提示用户输入用户名
fmt.Print("输入用户名:")
fmt.Scan(&user.Username)
// 提示用户输入密码, 并用变量接收用户输入的密码
fmt.Print("请输入密码:")
fmt.Scan(&user.Password)
return user
}
// 显示登录失败UI, 参数是错误消息, 返回是否重试
func DisplayLoginErr(msg string) bool {
PrintSysTitle(LOGIN_TITLE) // 显示系统标题
fmt.Println(">> [登录失败] ", msg) // 显示错误消息
// 显示用户选择菜单
fmt.Printf("\n(1). 重试\n(2). 退出\n")
// 获取用户输入
selected := ""
fmt.Print("请输入(1 或 2):")
fmt.Scan(&selected)
if selected == "1" {
return true
}
return false
}
在controller包下创建loginController.go文件, 里面用来写登录过程的控制:
package controller
import (
"ssms/model"
"ssms/service"
"ssms/view"
)
func Login() (model.User, bool) {
// 显示LoginUI获取用户输入
user := view.DisplayLoginUI()
// 登录校验
if !service.LoginValidate(user) { // 用户校验失败
retry := view.DisplayLoginErr("用户名或密码错误")
if retry {
return Login()
}
return user, false
} else { // 用户校验正确
// 取得用户信息
userInFile, count := service.GetUser(user.Username)
if count > 0 {
return userInFile, true
} else {
return user, false
}
}
}
新建一个test目录, 把strtest拖过去, 我们学习字符串操作的这些留着以后参考
最后把别的没用的都可以删了, 这个编程的框框粗略地弄好了, 下面开始写代码
程序框架
打开main.go文件, 会发现有报错, 因为我们把包都变了, 我们现在从initData函数开始梳理, 初始化函数明显属于Service共通部分的, 因此在service包中创建一个common.go文件:
package service
import (
"ssms/model"
"ssms/repository"
)
// 程序初始化
func InitData() bool {
// 初始化创建用户数据文件
ok := repository.CreateUserFile()
if !ok {
return false
}
// 初始化管理员用户
var admin = model.User{
Username: "admin",
Password: "123456",
Role: 9,
}
// 查找用户名是admin的用户
_, count := repository.ResearchUser(admin.Username)
if count <= 0 {
// 将默认管理员用户写入到用户数据文件中
ok = repository.CreateUser(admin)
}
// TODO 初始化其他数据文件
return true
}
仿照Login, 我们写一下显示主菜单的过程, 在view包里创建mainMenuView.go:
package view
import (
"fmt"
"ssms/model"
)
const (
TITLE = "主菜单"
MENU_EXAM = "(1). 考试管理"
MENU_SCOR = "(2). 成绩管理"
MENU_STUD = "(3). 学生管理"
MENU_USER = "(4). 用户管理"
MENU_SYST = "(5). 系统设置"
MENU_EXIT = "(0). 退出系统"
)
func DisPlayMainMenu(user model.User) {
// 显示标题
name := fmt.Sprintf("%s (%s)", TITLE, user.Username)
PrintSysTitle(name)
// 根据权限显示不同菜单
if user.Role == 9 {
fmt.Println(">> ", MENU_EXAM)
fmt.Println(">> ", MENU_SCOR)
fmt.Println(">> ", MENU_STUD)
fmt.Println(">> ", MENU_USER)
fmt.Println(">> ", MENU_SYST)
fmt.Println(">> ", MENU_EXIT)
} else if user.Role == 1 {
fmt.Println(">> ", MENU_EXAM)
fmt.Println(">> ", MENU_SCOR)
fmt.Println(">> ", MENU_STUD)
fmt.Println(">> ", MENU_SYST)
fmt.Println(">> ", MENU_EXIT)
} else {
fmt.Println(">> ", MENU_EXIT)
}
menu := 0
fmt.Print("\n请输入选择的功能:")
fmt.Scanf("%d", &menu)
if menu == 0 {
return
}
//TODO switch menu
}
在controller包里创建mainMenuController.go文件:
package controller
import (
"ssms/model"
"ssms/view"
)
func MainMenu(user model.User) {
// 显示标题
view.DisPlayMainMenu(user)
}
现在回来完成main函数, 可以看出逻辑非常清晰
// main函数, 程序入口函数
func main() {
// 初始化数据和数据文件
if !service.InitData() {
fmt.Println("程序初始化错误! 不能运行")
return
}
// 调用login控制器, 让用户完成登录过程
user, isLogin := controller.Login()
// 如果登录失败直接结束程序
if !isLogin {
return
}
// 登录成功 根据登录用户的权限显示主菜单
controller.MainMenu(user)
}
运行程序, 输错一次, 重试,再输入对一次,结果:
********************************************************************************
>> 学生成绩管理系统 - 用户登录
********************************************************************************
输入用户名:admin
请输入密码:111111
********************************************************************************
>> 学生成绩管理系统 - 用户登录
********************************************************************************
>> [登录失败] 用户名或密码错误
(1). 重试
(2). 退出
请输入(1 或 2):1
********************************************************************************
>> 学生成绩管理系统 - 用户登录
********************************************************************************
输入用户名:admin
请输入密码:123456
********************************************************************************
>> 学生成绩管理系统 - 主菜单 (admin)
********************************************************************************
>> (1). 考试管理
>> (2). 成绩管理
>> (3). 学生管理
>> (4). 用户管理
>> (5). 系统设置
>> (0). 退出系统
请输入选择的功能:0