聊天项目(11) 注册功能

注册功能

实现注册功能,先实现客户端发送post请求, 将注册ui中确定按钮改为sure_btn,并为其添加click槽函数

  1. //day11 添加确认槽函数
  2. void RegisterDialog::on_sure_btn_clicked()
  3. {
  4. if(ui->user_edit->text() == ""){
  5. showTip(tr("用户名不能为空"), false);
  6. return;
  7. }
  8. if(ui->email_edit->text() == ""){
  9. showTip(tr("邮箱不能为空"), false);
  10. return;
  11. }
  12. if(ui->pass_edit->text() == ""){
  13. showTip(tr("密码不能为空"), false);
  14. return;
  15. }
  16. if(ui->confirm_edit->text() == ""){
  17. showTip(tr("确认密码不能为空"), false);
  18. return;
  19. }
  20. if(ui->confirm_edit->text() != ui->pass_edit->text()){
  21. showTip(tr("密码和确认密码不匹配"), false);
  22. return;
  23. }
  24. if(ui->varify_edit->text() == ""){
  25. showTip(tr("验证码不能为空"), false);
  26. return;
  27. }
  28. //day11 发送http请求注册用户
  29. QJsonObject json_obj;
  30. json_obj["user"] = ui->user_edit->text();
  31. json_obj["email"] = ui->email_edit->text();
  32. json_obj["passwd"] = ui->pass_edit->text();
  33. json_obj["confirm"] = ui->confirm_edit->text();
  34. json_obj["varifycode"] = ui->varify_edit->text();
  35. HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/user_register"),
  36. json_obj, ReqId::ID_REG_USER,Modules::REGISTERMOD);
  37. }

再添加http请求回复后收到处理流程

  1. void RegisterDialog::initHttpHandlers()
  2. {
  3. //...省略
  4. //注册注册用户回包逻辑
  5. _handlers.insert(ReqId::ID_REG_USER, [this](QJsonObject jsonObj){
  6. int error = jsonObj["error"].toInt();
  7. if(error != ErrorCodes::SUCCESS){
  8. showTip(tr("参数错误"),false);
  9. return;
  10. }
  11. auto email = jsonObj["email"].toString();
  12. showTip(tr("用户注册成功"), true);
  13. qDebug()<< "email is " << email ;
  14. });
  15. }

Server端接受注册请求

Server注册user_register逻辑

  1. RegPost("/user_register", [](std::shared_ptr<HttpConnection> connection) {
  2. auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
  3. std::cout << "receive body is " << body_str << std::endl;
  4. connection->_response.set(http::field::content_type, "text/json");
  5. Json::Value root;
  6. Json::Reader reader;
  7. Json::Value src_root;
  8. bool parse_success = reader.parse(body_str, src_root);
  9. if (!parse_success) {
  10. std::cout << "Failed to parse JSON data!" << std::endl;
  11. root["error"] = ErrorCodes::Error_Json;
  12. std::string jsonstr = root.toStyledString();
  13. beast::ostream(connection->_response.body()) << jsonstr;
  14. return true;
  15. }
  16. //先查找redis中email对应的验证码是否合理
  17. std::string varify_code;
  18. bool b_get_varify = RedisMgr::GetInstance()->Get(src_root["email"].asString(), varify_code);
  19. if (!b_get_varify) {
  20. std::cout << " get varify code expired" << std::endl;
  21. root["error"] = ErrorCodes::VarifyExpired;
  22. std::string jsonstr = root.toStyledString();
  23. beast::ostream(connection->_response.body()) << jsonstr;
  24. return true;
  25. }
  26. if (varify_code != src_root["varifycode"].asString()) {
  27. std::cout << " varify code error" << std::endl;
  28. root["error"] = ErrorCodes::VarifyCodeErr;
  29. std::string jsonstr = root.toStyledString();
  30. beast::ostream(connection->_response.body()) << jsonstr;
  31. return true;
  32. }
  33. //访问redis查找
  34. bool b_usr_exist = RedisMgr::GetInstance()->ExistsKey(src_root["user"].asString());
  35. if (b_usr_exist) {
  36. std::cout << " user exist" << std::endl;
  37. root["error"] = ErrorCodes::UserExist;
  38. std::string jsonstr = root.toStyledString();
  39. beast::ostream(connection->_response.body()) << jsonstr;
  40. return true;
  41. }
  42. //查找数据库判断用户是否存在
  43. root["error"] = 0;
  44. root["email"] = src_root["email"];
  45. root ["user"]= src_root["user"].asString();
  46. root["passwd"] = src_root["passwd"].asString();
  47. root["confirm"] = src_root["confirm"].asString();
  48. root["varifycode"] = src_root["varifycode"].asString();
  49. std::string jsonstr = root.toStyledString();
  50. beast::ostream(connection->_response.body()) << jsonstr;
  51. return true;
  52. });

安装Mysql

先介绍Windows环境下安装mysql

点击mysql安装包下载链接:https://dev.mysql.com/downloads/mysql

选择window版本,点击下载按钮,如下所示

https://cdn.llfc.club/4aa44fdafe578d8f2626d3e280d608f.png

不用登录直接下载

https://cdn.llfc.club/1711349001944.jpg

下载好mysql安装包后,将其解压到指定目录,并记下解压的目录,后续用于环境变量配置

https://cdn.llfc.club/1711349518362.jpg

在bin目录同级下创建一个文件,命名为my.ini
编辑my.ini文件

  1. [mysqld]
  2. # 设置3308端口
  3. port=3308
  4. # 设置mysql的安装目录 ---这里输入你安装的文件路径----
  5. basedir=D:\cppsoft\mysql
  6. # 设置mysql数据库的数据的存放目录
  7. datadir=D:\mysql\data
  8. # 允许最大连接数
  9. max_connections=200
  10. # 允许连接失败的次数。
  11. max_connect_errors=10
  12. # 服务端使用的字符集默认为utf8
  13. character-set-server=utf8
  14. # 创建新表时将使用的默认存储引擎
  15. default-storage-engine=INNODB
  16. # 默认使用“mysql_native_password”插件认证
  17. #mysql_native_password
  18. default_authentication_plugin=mysql_native_password
  19. [mysql]
  20. # 设置mysql客户端默认字符集
  21. default-character-set=utf8
  22. [client]
  23. # 设置mysql客户端连接服务端时默认使用的端口
  24. port=3308
  25. default-character-set=utf8

有两点需要注意修改的:

A、basedir这里输入的是mysql解压存放的文件路径

B、datadir这里设置mysql数据库的数据存放目录

打开cmd进入mysql的bin文件下

https://cdn.llfc.club/1711349826275.jpg

依次执行命令

第一个命令为:

  1. //安装mysql 安装完成后Mysql会有一个随机密码
  2. .\mysqld.exe --initialize --console

如下图,随机密码要记住,以后我们改密码会用到

https://cdn.llfc.club/83635680847f591980ade3501655f8d.png

接下来在cmd执行第二条命令

  1. //安装mysql服务并启动
  2. .\mysqld.exe --install mysql

如果出现以下情况,说明cmd不是以管理员形式执行的,改用为管理员权限执行即可。

https://cdn.llfc.club/2872369cb66fa7803e19575be3cd63b.png

成功如下

https://cdn.llfc.club/87a224f42f4dccb254481470d2f1b8e.png

目前为止安装完毕,大家如果mysql官网下载缓慢,可以去我的网盘下载

https://pan.baidu.com/s/1BTMZB31FWFUq4mZZdzcA9g?pwd=6xlz

提取码:6xlz

修改mysql密码

1 在本机启动mysql服务:

点击桌面我的电脑,右键选择管理进去:

https://cdn.llfc.club/1711350803255.jpg

点击后选择服务

https://cdn.llfc.club/1711350871137.jpg

点击服务后可查看当前计算机启动的所有服务,找到mysql,然后右键点击设为启动,同时也可设置其为自动启动和手动启动

https://cdn.llfc.club/1711350989964.jpg

继续在cmd上执行以下命令

  1. mysql -uroot -p

回车后输入上面安装时保存的初始密码,进入mysql里面:

https://cdn.llfc.club/b33134d93210412a6d301c9eedfa8a5.png

在mysql里面继续执行以下命令:

  1. //修改密码为123mysql
  2. ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';

回车按照指引执行完后,代表密码修改成功,再输入exit;退出即可

配置环境变量

为了方便使用mysql命令,可以将mysql目录配置在环境变量里

新建系统变量:

变量名:MYSQL_HOME

变量值:msql目录

https://cdn.llfc.club/1711352568377.jpg

修改系统的path变量

编辑path,进去后添加 %MYSQL_HOME%\bin

https://cdn.llfc.club/1711352718673.jpg

测试连接

为了方便测试,大家可以使用navicat等桌面工具测试连接。以后增删改查也方便。

可以去官网下载

https://www.navicat.com.cn/

或者我得网盘下载

https://pan.baidu.com/s/10jApYUrwaI19j345dpPGNA?pwd=77m2

验证码: 77m2

效果如下:

https://cdn.llfc.club/1711531330919.jpg

Docker环境配置mysql

拉取mysql镜像

  1. docker pull mysql:8.0

先启动一个测试版本,然后把他的配置文件拷贝出来

  1. docker run --name mysqltest \
  2. -p 3307:3306 -e MYSQL_ROOT_PASSWORD=root \
  3. -d mysql

创建三个目录,我得目录是

  1. mkdir -p /home/zack/llfc/mysql/config
  2. mkdir -p /home/zack/llfc/mysql/data
  3. mkdir -p /home/zack/llfc/mysql/logs

进入docker中

  1. docker exec -it mysqltest bash

之后可以通过搜寻找到配置在/etc/mysql/my.cnf

所以接下来退出容器,执行拷贝命令

  1. docker cp mysqltest:/etc/mysql/my.cnf /home/zack/llfc/mysql/config

然后删除测试用的mysql docker

  1. docker rm -f mysqltest

然后启动我们的容器

  1. docker run --restart=on-failure:3 -d \
  2. -v /home/zack/llfc/mysql/config/my.cnf:/etc/mysql/my.cnf \
  3. -v /home/zack/llfc/mysql/data/:/var/lib/mysql \
  4. -v /home/zack/llfc/mysql/logs:/logs -p 3308:3306 \
  5. --name llfcmysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0

设置远程访问

进入docker

  1. docker exec -it llfcmysql bash

登录mysql

  1. mysql -u root -p

设置允许远程访问,我不设置也能访问的,这里介绍一下。

  1. use mysql
  2. ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
  3. flush privileges;

再次用navicat连接,是可以连接上了。

完善GateServer配置

添加Redis和Mysql配置

  1. [Mysql]
  2. Host = 81.68.86.146
  3. Port = 3308
  4. Passwd = 123456
  5. [Redis]
  6. Host = 81.68.86.146
  7. Port = 6380
  8. Passwd = 123456

Mysql Connector C++

尽管Mysql提供了访问数据库的接口,但是都是基于C风格的,为了便于面向对象设计,我们使用Mysql Connector C++ 这个库来访问mysql。

我们先安装这个库,因为我们windows环境代码是debug版本,所以下载connector的debug版本,如果你的开发编译用的release版本,那么就要下载releas版本,否则会报错
terminate called after throwing an instance of 'std::bad_alloc'.

因为我在windows只做debug调试后期会将项目移植到Linux端,所以这里只下载debug版

下载地址

https://dev.mysql.com/downloads/connector/cpp/

如果下载缓慢可以去我的网盘下载
https://pan.baidu.com/s/1XAVhPAAzZpZahsyITua2oQ?pwd=9c1w

提取码:9c1w

https://cdn.llfc.club/1711692126532.jpg

下载后将文件夹解压放在一个自己常用的目录,我放在D:\cppsoft\mysql_connector

https://cdn.llfc.club/1711692478215.jpg

接下来去visual studio中配置项目

VC++ 包含目录添加D:\cppsoft\mysql_connector\include

https://cdn.llfc.club/1711692778937.jpg

库目录包含D:\cppsoft\mysql_connector\lib64\vs14

https://cdn.llfc.club/1711693069494.jpg

然后将D:\cppsoft\mysql_connector\lib64\debug下的mysqlcppconn8-2-vs14.dll和mysqlcppconn9-vs14.dll分别拷贝到项目中

为了让项目自动将dll拷贝到运行目录,可以在生成事件->生成后事件中添加xcopy命令

https://cdn.llfc.club/1711693404656.jpg

  1. xcopy $(ProjectDir)config.ini $(SolutionDir)$(Platform)\$(Configuration)\ /y
  2. xcopy $(ProjectDir)*.dll $(SolutionDir)$(Platform)\$(Configuration)\ /y

封装mysql连接池

  1. class MySqlPool {
  2. public:
  3. MySqlPool(const std::string& url, const std::string& user, const std::string& pass, const std::string& schema, int poolSize)
  4. : url_(url), user_(user), pass_(pass), schema_(schema), poolSize_(poolSize), b_stop_(false){
  5. try {
  6. for (int i = 0; i < poolSize_; ++i) {
  7. sql::mysql::MySQL_Driver* driver = sql::mysql::get_mysql_driver_instance();
  8. std::unique_ptr<sql::Connection> con(driver->connect(url_, user_, pass_));
  9. con->setSchema(schema_);
  10. pool_.push(std::move(con));
  11. }
  12. }
  13. catch (sql::SQLException& e) {
  14. // 处理异常
  15. std::cout << "mysql pool init failed" << std::endl;
  16. }
  17. }
  18. std::unique_ptr<sql::Connection> getConnection() {
  19. std::unique_lock<std::mutex> lock(mutex_);
  20. cond_.wait(lock, [this] {
  21. if (b_stop_) {
  22. return true;
  23. }
  24. return !pool_.empty(); });
  25. if (b_stop_) {
  26. return nullptr;
  27. }
  28. std::unique_ptr<sql::Connection> con(std::move(pool_.front()));
  29. pool_.pop();
  30. return con;
  31. }
  32. void returnConnection(std::unique_ptr<sql::Connection> con) {
  33. std::unique_lock<std::mutex> lock(mutex_);
  34. if (b_stop_) {
  35. return;
  36. }
  37. pool_.push(std::move(con));
  38. cond_.notify_one();
  39. }
  40. void Close() {
  41. b_stop_ = true;
  42. cond_.notify_all();
  43. }
  44. ~MySqlPool() {
  45. std::unique_lock<std::mutex> lock(mutex_);
  46. while (!pool_.empty()) {
  47. pool_.pop();
  48. }
  49. }
  50. private:
  51. std::string url_;
  52. std::string user_;
  53. std::string pass_;
  54. std::string schema_;
  55. int poolSize_;
  56. std::queue<std::unique_ptr<sql::Connection>> pool_;
  57. std::mutex mutex_;
  58. std::condition_variable cond_;
  59. std::atomic<bool> b_stop_;
  60. };

封装DAO操作层

类的声明

  1. class MysqlDao
  2. {
  3. public:
  4. MysqlDao();
  5. ~MysqlDao();
  6. int RegUser(const std::string& name, const std::string& email, const std::string& pwd);
  7. private:
  8. std::unique_ptr<MySqlPool> pool_;
  9. };

实现

  1. MysqlDao::MysqlDao()
  2. {
  3. auto & cfg = ConfigMgr::Inst();
  4. const auto& host = cfg["Mysql"]["Host"];
  5. const auto& port = cfg["Mysql"]["Port"];
  6. const auto& pwd = cfg["Mysql"]["Passwd"];
  7. const auto& schema = cfg["Mysql"]["Schema"];
  8. const auto& user = cfg["Mysql"]["User"];
  9. pool_.reset(new MySqlPool(host+":"+port, user, pwd,schema, 5));
  10. }
  11. MysqlDao::~MysqlDao(){
  12. pool_->Close();
  13. }
  14. int MysqlDao::RegUser(const std::string& name, const std::string& email, const std::string& pwd)
  15. {
  16. auto con = pool_->getConnection();
  17. try {
  18. if (con == nullptr) {
  19. pool_->returnConnection(std::move(con));
  20. return false;
  21. }
  22. // 准备调用存储过程
  23. unique_ptr < sql::PreparedStatement > stmt(con->prepareStatement("CALL reg_user(?,?,?,@result)"));
  24. // 设置输入参数
  25. stmt->setString(1, name);
  26. stmt->setString(2, email);
  27. stmt->setString(3, pwd);
  28. // 由于PreparedStatement不直接支持注册输出参数,我们需要使用会话变量或其他方法来获取输出参数的值
  29. // 执行存储过程
  30. stmt->execute();
  31. // 如果存储过程设置了会话变量或有其他方式获取输出参数的值,你可以在这里执行SELECT查询来获取它们
  32. // 例如,如果存储过程设置了一个会话变量@result来存储输出结果,可以这样获取:
  33. unique_ptr<sql::Statement> stmtResult(con->createStatement());
  34. unique_ptr<sql::ResultSet> res(stmtResult->executeQuery("SELECT @result AS result"));
  35. if (res->next()) {
  36. int result = res->getInt("result");
  37. cout << "Result: " << result << endl;
  38. pool_->returnConnection(std::move(con));
  39. return result;
  40. }
  41. pool_->returnConnection(std::move(con));
  42. return -1;
  43. }
  44. catch (sql::SQLException& e) {
  45. pool_->returnConnection(std::move(con));
  46. std::cerr << "SQLException: " << e.what();
  47. std::cerr << " (MySQL error code: " << e.getErrorCode();
  48. std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
  49. return -1;
  50. }
  51. }

新建数据库llfc, llfc数据库添加user表和user_id表

https://cdn.llfc.club/1712109915609.jpg

user表
https://cdn.llfc.club/1712109796859.jpg

user_id就一行数据,用来记录用户id

https://cdn.llfc.club/1712110047125.jpg

这里id用简单计数表示,不考虑以后合服务器和分表分库,如果考虑大家可以采取不同的策略,雪花算法等。

新建存储过程

  1. CREATE DEFINER=`root`@`%` PROCEDURE `reg_user`(
  2. IN `new_name` VARCHAR(255),
  3. IN `new_email` VARCHAR(255),
  4. IN `new_pwd` VARCHAR(255),
  5. OUT `result` INT)
  6. BEGIN
  7. -- 如果在执行过程中遇到任何错误,则回滚事务
  8. DECLARE EXIT HANDLER FOR SQLEXCEPTION
  9. BEGIN
  10. -- 回滚事务
  11. ROLLBACK;
  12. -- 设置返回值为-1,表示错误
  13. SET result = -1;
  14. END;
  15. -- 开始事务
  16. START TRANSACTION;
  17. -- 检查用户名是否已存在
  18. IF EXISTS (SELECT 1 FROM `user` WHERE `name` = new_name) THEN
  19. SET result = 0; -- 用户名已存在
  20. COMMIT;
  21. ELSE
  22. -- 用户名不存在,检查email是否已存在
  23. IF EXISTS (SELECT 1 FROM `user` WHERE `email` = new_email) THEN
  24. SET result = 0; -- email已存在
  25. COMMIT;
  26. ELSE
  27. -- email也不存在,更新user_id
  28. UPDATE `user_id` SET `id` = `id` + 1;
  29. -- 获取更新后的id
  30. SELECT `id` INTO @new_id FROM `user_id`;
  31. -- user表中插入新记录
  32. INSERT INTO `user` (`uid`, `name`, `email`, `pwd`) VALUES (@new_id, new_name, new_email, new_pwd);
  33. -- 设置result为新插入的uid
  34. SET result = @new_id; -- 插入成功,返回新的uid
  35. COMMIT;
  36. END IF;
  37. END IF;
  38. END

数据库管理者

我们需要建立一个数据库管理者用来实现服务层,对接逻辑层的调用

  1. #include "const.h"
  2. #include "MysqlDao.h"
  3. class MysqlMgr: public Singleton<MysqlMgr>
  4. {
  5. friend class Singleton<MysqlMgr>;
  6. public:
  7. ~MysqlMgr();
  8. int RegUser(const std::string& name, const std::string& email, const std::string& pwd);
  9. private:
  10. MysqlMgr();
  11. MysqlDao _dao;
  12. };

实现

  1. #include "MysqlMgr.h"
  2. MysqlMgr::~MysqlMgr() {
  3. }
  4. int MysqlMgr::RegUser(const std::string& name, const std::string& email, const std::string& pwd)
  5. {
  6. return _dao.RegUser(name, email, pwd);
  7. }
  8. MysqlMgr::MysqlMgr() {
  9. }

逻辑层调用

在逻辑层注册消息处理。

  1. RegPost("/user_register", [](std::shared_ptr<HttpConnection> connection) {
  2. auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
  3. std::cout << "receive body is " << body_str << std::endl;
  4. connection->_response.set(http::field::content_type, "text/json");
  5. Json::Value root;
  6. Json::Reader reader;
  7. Json::Value src_root;
  8. bool parse_success = reader.parse(body_str, src_root);
  9. if (!parse_success) {
  10. std::cout << "Failed to parse JSON data!" << std::endl;
  11. root["error"] = ErrorCodes::Error_Json;
  12. std::string jsonstr = root.toStyledString();
  13. beast::ostream(connection->_response.body()) << jsonstr;
  14. return true;
  15. }
  16. auto email = src_root["email"].asString();
  17. auto name = src_root["user"].asString();
  18. auto pwd = src_root["passwd"].asString();
  19. auto confirm = src_root["confirm"].asString();
  20. if (pwd != confirm) {
  21. std::cout << "password err " << std::endl;
  22. root["error"] = ErrorCodes::PasswdErr;
  23. std::string jsonstr = root.toStyledString();
  24. beast::ostream(connection->_response.body()) << jsonstr;
  25. return true;
  26. }
  27. //先查找redis中email对应的验证码是否合理
  28. std::string varify_code;
  29. bool b_get_varify = RedisMgr::GetInstance()->Get(CODEPREFIX+src_root["email"].asString(), varify_code);
  30. if (!b_get_varify) {
  31. std::cout << " get varify code expired" << std::endl;
  32. root["error"] = ErrorCodes::VarifyExpired;
  33. std::string jsonstr = root.toStyledString();
  34. beast::ostream(connection->_response.body()) << jsonstr;
  35. return true;
  36. }
  37. if (varify_code != src_root["varifycode"].asString()) {
  38. std::cout << " varify code error" << std::endl;
  39. root["error"] = ErrorCodes::VarifyCodeErr;
  40. std::string jsonstr = root.toStyledString();
  41. beast::ostream(connection->_response.body()) << jsonstr;
  42. return true;
  43. }
  44. //查找数据库判断用户是否存在
  45. int uid = MysqlMgr::GetInstance()->RegUser(name, email, pwd);
  46. if (uid == 0 || uid == -1) {
  47. std::cout << " user or email exist" << std::endl;
  48. root["error"] = ErrorCodes::UserExist;
  49. std::string jsonstr = root.toStyledString();
  50. beast::ostream(connection->_response.body()) << jsonstr;
  51. return true;
  52. }
  53. root["error"] = 0;
  54. root["uid"] = uid;
  55. root["email"] = email;
  56. root ["user"]= name;
  57. root["passwd"] = pwd;
  58. root["confirm"] = confirm;
  59. root["varifycode"] = src_root["varifycode"].asString();
  60. std::string jsonstr = root.toStyledString();
  61. beast::ostream(connection->_response.body()) << jsonstr;
  62. return true;
  63. });

再次启动客户端测试,可以注册成功

热门评论

热门文章

  1. 解密定时器的实现细节

    喜欢(566) 浏览(1882)
  2. C++ 类的继承封装和多态

    喜欢(588) 浏览(2610)
  3. slice介绍和使用

    喜欢(521) 浏览(1741)
  4. Linux环境搭建和编码

    喜欢(594) 浏览(5593)
  5. windows环境搭建和vscode配置

    喜欢(587) 浏览(1716)

最新评论

  1. C++ 类的拷贝构造、赋值运算、单例模式 secondtonone1:好的,已修复。
  2. 双链表实现LRU算法 secondtonone1:双链表插入和删除节点是本篇的难点,多多练习即可。
  3. 线程安全的无锁栈 secondtonone1:谢谢支持,如果pop的次数大于push的次数是会让线程处于重试的,这个是测试用例,必须满足push和pop的次数相同,实际情况不会这么使用。栈的设计没有问题。
  4. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  5. Linux环境搭建和编码 恋恋风辰:Linux环境下go的安装比较简单,可以不用设置GOPATH环境变量,后期我们学习go mod 之后就拜托了go文件目录的限制了。

个人公众号

个人微信