[{"content":"","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/tags/cg/","section":"Tags","summary":"","title":"CG","type":"tags"},{"content":" 声明：部分内容参考网上博客。\n一些笔记参考:\nTA学习笔记\nGAMES101笔记_Lec05~06_光栅化 Rasterization - Ziur的文章 - 知乎\n# 计算机图形学【GAMES-101】2、光栅化(反走样、傅里叶变换、卷积)\n采样 Sampling # 光栅化 = 采样二维平面上的点 摄影 = 采样感光元件上的信息(将光学信息离散为图像上的像素) 视频 = 在时间时间中的采样 采样带来的失真 # 锯齿 由于采样率对于信号来说不够高，造成了信号的走样问题，形成了锯齿 摩尔纹 左图跳过奇数行和奇数列后得到右图 车轮效应 对高速旋转的物体采样可能出现视觉上倒转的现象 失真的原因 # 信号频率太快，采样太慢。\n反走样 Antialiasing # 流程总览 原始连续几何信号 → 预滤波（Pre-Filter） → 采样（Sample） → 抗锯齿结果\n反采样的顺序: 先滤波，再采样\n分步拆解 步骤 操作 原理 效果 原始三角形（左） 连续的几何信号，边缘锐利突变 包含大量高频成分，直接采样会产生严重走样（锯齿） 边缘锐利，无过渡 预滤波（中） 低通滤波（高斯模糊 / 盒式滤波） 去除高于奈奎斯特频率的高频成分，从根源避免频谱混叠 边缘柔化，出现红→白的平滑渐变 采样（右） 对滤波后信号做离散像素采样 像素根据覆盖比例 / 滤波结果取中间色值 边缘像素呈现深浅不一的红色，视觉平滑无锯齿 频域 (Frequency Domain) # 频域（frequency-domain）信号是描述信号在频率方面特性的坐标系。\n横轴是频率,纵轴是频率变化的幅度\n傅里叶变换Fourier Transform # 傅里叶级数展开 # 任何一个周期函数都可以写成一系列正余弦函数的组合+一个常数项，我们可以用傅里叶展开式来近似端数字的信号，随着展开项增多，图像会越来越接贴合。\n傅里叶变换 # 傅里叶展开和傅里叶变换不是一回事，但是很相似\n傅里叶变换可以把一个函数从时域变到频域 任何周期函数，都可以看作是不同振幅、相位的正弦波的叠加，比如某个周期函数经过傅里叶变换后得到下面五个函数的叠加，频率递增。用同样的采样频率采样这五个函数，随着信号频率递增，采样结果越来越表现不了原来信号的图像，所以更高频率的信号需要更快的采样速度！\n而采样跟不上频率则会产生走样现象.\n可以看到在采样确定的情况下,频率增加,导致同样的采样下无法区别出不同频率的曲线. 具体傅里叶分析知识请看Heinrich大佬的讲解\n图像与频域 # 这是数字图像处理中二维傅里叶变换（2D Fourier Transform）的经典演示，左右两图对应空间域与频率域，是频域分析入门示例。\n左右图解析*\n左图：空间域（原始图像）\n本质：灰度人像照片，是图像的空间 / 时域直观表达 特征：像素灰度值直接对应位置亮度，可直观识别人物、动作、背景 右图：频率域（幅度谱）\n本质：左图经 2D 傅里叶变换生成的幅度谱，是频率域表示 转换逻辑：将 “像素位置 + 亮度” 的空间描述，转为 “频率分量 + 幅度 / 相位” 的频率描述 关键处理：经频谱中心化（fftshift），低频移至中心，高频分布在四周 频谱图核心解读\n1. 中心亮斑（低频分量）\n对应特征：图像亮度变化平缓的区域（皮肤、大面积色块、模糊背景） 能量特点：中心亮度最高，低频能量占比最大，决定图像主体结构 星芒线条：由图像水平 / 垂直边缘（轮廓、褶皱、边界）产生，对应高频分量的方向特征 2. 四周暗区（高频分量）\n对应特征：亮度变化剧烈的区域（头发丝、手指边缘、纹理、噪点） 能量特点：高频能量远低于低频，画面整体偏暗，仅中心附近有微弱细节 3. 十字辅助线\n标注频谱中心（原点），区分水平 / 垂直频率轴，辅助理解频域坐标体系\n核心意义与误区\n核心意义\n频率分离：拆分图像整体结构（低频）与细节 / 噪声（高频），为后续处理奠基 频域滤波：实现低通（模糊 / 去噪）、高通（锐化）、去除周期性噪声等操作 特征提取：用于图像压缩、特征匹配、目标检测等任务 常见误区\n❌ 错误：中心为高频、周围为低频 ✅ 正确：中心化后中心低频，越往四周频率越高 补充：频谱含幅度谱（展示亮度分布）与相位谱（决定图像结构），后者是图像结构的核心 滤波 Filtering # 过滤某种频率的信号。\n高通滤波（high-pass filter） # 只有高频信号可以通过，只剩下高频信息，丢掉低频信息 特征：仅保留了图像的高频信息（亮度剧烈突变的区域），即人物轮廓、发丝、手指边缘、衣服褶皱等细节；低频的平滑区域（皮肤、大面积色块、背景）被完全滤除，呈现黑底白边的边缘效果。 低通滤波（low-pass filter） # 只有低频信号可以通过，只剩下低频信息，丢掉高频信息 带通滤波 ( band-pass filter) # 带通滤波，允许限定频段的波通过，通常是将一张图片经过傅里叶变换得到频域，然后去掉高于限定的最高频和低于限定的最低频信号，如下图中右侧，像一个圈圈一样，经过逆傅里叶变换就可以得到左侧的图像。 卷积 Convolution # 卷积在图形学中简化的理解 卷积实际上就是定义一个滤波器(滤波器也被称之为卷积核)，这个滤波器可以是一维数组也可以是二维数组，使用这个滤波器对原来的信号挨个进行处理，然后把处理好的结果写进一个与原数据相同大小的容器中，滤波的本质是将信号与滤波器在时域上进行卷积的操作。(form https://www.yuque.com/gaoshanliushui-mbfny/sst4c5/agupih#3cf7f234)\n卷积的定义 # 原始信号的任意一个位置，取其周围的平均 作用在一个信号上，用一种滤波操作，得到一个结果Result 卷积定理 # 空域中的卷积 = 频域中的乘积，反之亦然（vice versa）\nOption 1：空域卷积滤波（直接法，对应上图的滑动窗口以及下图中的上半部分）\n原理：用卷积核（滤波器）在图像 / 信号上逐像素滑动，对邻域做加权求和，生成新像素值（一维信号→二维图像的直接扩展） 图形学对应：实时渲染中直接在像素空间实现滤波，如 3×3 高斯模糊、Sobel 边缘检测、抗锯齿的超采样 计算复杂度：O(N2K2)（N为图像尺寸，K为卷积核尺寸），核越大计算呈平方级变慢 Option 2：频域乘积滤波（变换法，基于卷积定理,下图中的下半部分）\n分三步实现，完全对应图中流程：\n傅里叶变换（FFT）：将原始图像 f(x,y) 和卷积核 g(x,y) 从空域变换到频域，得到频谱 F{f} 和 F{g} 频域逐元素相乘：将两个频谱逐点相乘，得到滤波后的频域结果 F{f}⋅F{g} 逆傅里叶变换（IFFT）：将乘积结果变换回空域，得到最终滤波后的图像 f∗g 计算复杂度：O(N2logN)（与卷积核大小无关，仅和图像尺寸相关） 卷积盒(Box Filter) # 乘以1/9的原因——对处理结果进行归一化：卷积操作时原图像中的9个像素卷积盒中的元素分别相乘再相加，得到的是原来的9倍，会导致图像异常明亮。\n图像中小的正方体对应低通滤波器，更大的正方体对应只允许更低频率通过的滤波器\n核越宽（空域越大），频域主瓣越窄，通带频率越低，模糊越强；核越窄，通带越宽，模糊越弱。 从频域看采样 # 采样(Sampling)：采样就是重复频率上的内容\n从原理分析如何减少反走样 # 走样的本质\n采样速度不够快,导致信号中间间隔小,进而导致信号混在一起,形成走样\n$$ \\boldsymbol{\\text{频谱间隔} = f_s = \\frac{1}{T}} $$ 采样间隔 T 增大 → 采样率 fs​=1/T 降低 → 原始频谱的周期延拓周期缩短 → 相邻频谱副本之间的保护间隔减小\n如何做反走样 # 方法一：增加采样率 本质上增加了采样时频谱中副本之间的距离 使用更高分辨率的显示器、传感器、更高规格的帧缓冲区 但是：成本非常高，可能需要非常高的分辨率\n方法二：反走样 Antialiasing 反走样：先模糊再采样\n首先进行低通滤波,去除高频信号,最大限度保留信号原有的样子,又缩小间隔，不会发生混叠，进而达到反走样的目的。\n多重采样抗锯齿 Multisample Antialiasing (MSAA) # 先提一下 超采样抗锯齿(Super Sample Anti-aliasing, SSAA)\nSSAA 是最原始、效果最好但也最消耗性能的抗锯齿技术。\n工作原理\n它的原理非常直接：“先放大，再缩小”。\n显卡会以远高于显示器分辨率的规格（例如 4 倍，即 4xSSAA）来渲染整个游戏场景。 渲染完成后，再将这个超高分辨率的图像缩小到显示器原生分辨率进行输出。 在缩小的过程中，原本锯齿状的边缘会因为像素的混合而变得非常平滑。 优点\n画质顶级：因为它对整个场景的所有像素（包括物体边缘、半透明材质、纹理细节等）都进行了超采样，所以抗锯齿效果是最全面、最完美的。 缺点\n性能开销巨大：显卡需要渲染的像素量成倍增加，对 GPU 和显存带宽的压力极大，会导致游戏帧率严重下降。因此，它在实时游戏中很少被使用，更多见于对画质要求极高的离线渲染（如电影特效）。 多重采样抗锯齿(Multisample Anti-aliasing, MSAA)，借鉴SSAA的理念，以更加高效的方法实现抗锯齿。其具体实现方法如下：\n原本一个像素只在其中心点采样一次，是不可能知道每个像素的覆盖率的，因为非0即1 这时候需要使用更多的像素点来检测每个像素的覆盖率.需要注意的是这些像素点是虚拟的,用来作为采样存在的.\n原本的inside(x,y,t)传入的点不再是像素中心点(x+0.5,y+0.5) 而是四个子采样点坐标(x+0.25,y+0.25)，(x+0.25,y+0.75)，(x+0.75,y+0.25)，(x+0.75,y+0.75) 若一个像素被覆盖n个子采样点，则该像素 覆盖率 为(n/4)x100%\n覆盖率计算完毕则得到每个像素被三角形覆盖的百分比， ==到这里只是模糊操作完毕== 模糊后再进行采样(采样每个像素的颜色)\n像素的颜色=覆盖率x三角形颜色 MSAA绝对不是增加分辨率来解决走样问题，增加采样点的目的只是为了算覆盖率\n详尽的对于MSAA的讲解可以去看知乎的这篇文章 : 深入剖析MSAA - fengliancanxue的文章 - 知乎\n快速近似抗锯齿 Fast Approximate Anti-Aliasing (FXAA) # 它是一种和采样无关，在图像层面做的处理，处理过程是先找到三角形的边界，把有锯齿的边界替换为没有锯齿的边界，而且处理起来非常快.\n时间抗锯齿 Temporal Anti-Aliasing (TAA) # 最大的特点就是非常快速，是将静态的图片在时间上进行采样，图像不错MSAA，相连两帧显示的图像是一样，但是可以用相邻两帧同一个像素上不同位置的点来感知是否在三角形内，计算的时候要考虑上一帧感知的结果要被应用进来，相当于是MSAA对应的样本分布在时间上，并且当前这帧没有任何额外的操作\n深度学习超级采样 Deep Learning Super Sampling (DLSS) # 它依赖深度学习，使用低分辨率图像(比如1080p)生成高分辨率图像(8K)，再把8K图像缩回4K，得到抗锯齿图像，以代替传统的时间抗锯齿等技术\n可见度/遮挡 # 画家算法: (对三角形排序)先画远处的三角形，然后近处的三角形覆盖远处.\n但是画家算法会碰到一个问题,不适合在计算机中绘制互相遮挡的图形，如下的三角形.\nZ-buffer 深度缓存 # Z-buffer 无法处理透明物体\n不对三角形进行排序，在每个像素上对z值 比大小 用一块缓存，存放每个像素的深度信息 得到最终渲染图像的同时会有一个深度缓存产生 1、frame buffer 存放每个像素的颜色 2、depth buffer 存放每个像素的深度信息(也叫z-buffer) 注意相机看向-z轴，为了方便计算对z轴数值取绝对值，z越大越远 伪代码: 图像示例: 近像素（小）盖住远像素（大）\n更小-\u0026gt;更新 更大-\u0026gt;不变 相同：深度冲突 ","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/posts/games101_06/","section":"Posts","summary":"","title":"GAMES101_06: 光栅化(深度测试与抗锯齿)","type":"posts"},{"content":"","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/","section":"Xenolies Blog","summary":"","title":"Xenolies Blog","type":"page"},{"content":"","date":"2026 年 03 月 16 日","externalUrl":null,"permalink":"/categories/%E7%AC%94%E8%AE%B0/","section":"Categories","summary":"","title":"笔记","type":"categories"},{"content":" 题目一 # get_model_matrix(float rotation_angle): 逐个元素地构建模型变换矩 阵并返回该矩阵。在此函数中，你只需要实现三维中绕 z 轴旋转的变换矩阵， 而不用处理平移与缩放。\n根据这个公式编写代码 $$ \\text{绕z轴旋转: } \\mathbf{R}_z(\\alpha) = \\begin{pmatrix} \\cos\\alpha \u0026 -\\sin\\alpha \u0026 0 \u0026 0 \\\\ \\sin\\alpha \u0026 \\cos\\alpha \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 由于给的值是角度,而numext 接受的值是弧度所以需要角度转为弧度\n$$ \\text{弧度} = \\text{角度} \\times \\frac{\\pi}{180} $$然后使用三维仿射变换的旋转公式编写绕z轴旋转的代码\n$$ \\text{绕z轴旋转: } \\mathbf{R}_z(\\alpha) = \\begin{pmatrix} \\cos\\alpha \u0026 -\\sin\\alpha \u0026 0 \u0026 0 \\\\ \\sin\\alpha \u0026 \\cos\\alpha \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ float rad = rotation_angle * (MY_PI / 180); Eigen::Matrix4f rotation; // 旋转矩阵中numext::sin的三角函数接收的是弧度需要将角度转为弧度:弧度=角度*(PI/180) rotation \u0026lt;\u0026lt; numext::cos(rad), -numext::sin(rad), 0, 0, // 第一行 numext::sin(rad), numext::cos(rad), 0, 0, // 第二行 0, 0, 1, 0, //第三行 0, 0, 0, 1; 函数完整代码\n//模型矩阵 参数是一个旋转的角度 Eigen::Matrix4f get_model_matrix(float rotation_angle) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); // TODO: 实现此函数 // 创建绕 Z 轴旋转三角形的模型矩阵。 // 然后返回该矩阵。 float rad = rotation_angle * (MY_PI / 180); Eigen::Matrix4f rotation; // 旋转矩阵中numext::sin的三角函数接收的是弧度需要将角度转为弧度:弧度=角度*(PI/180) rotation \u0026lt;\u0026lt; numext::cos(rad), -numext::sin(rad), 0, 0, // 第一行 numext::sin(rad), numext::cos(rad), 0, 0, // 第二行 0, 0, 1, 0, //第三行 0, 0, 0, 1; model = rotation * model; return model; } 题目二 # get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar): 使用给定的参数逐个元素地构建透视投影矩阵并返回 该矩阵。 使用透视投影矩阵的公式\n$$ M_{\\text{persp}} = M_{\\text{ortho}} \\cdot M_{\\text{persp} \\to \\text{ortho}} $$可以看到透视投影矩阵相当于 正交矩阵 与 变换矩阵相乘,需要先写出正交矩阵\n$$ M_{\\text{ortho}} = \\underbrace{ \\begin{bmatrix} \\frac{2}{r-l} \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 \\frac{2}{t-b} \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 \\frac{2}{n-f} \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\text{缩放矩阵 } S} \\cdot \\underbrace{ \\begin{bmatrix} 1 \u0026 0 \u0026 0 \u0026 -\\frac{r+l}{2} \\\\ 0 \u0026 1 \u0026 0 \u0026 -\\frac{t+b}{2} \\\\ 0 \u0026 0 \u0026 1 \u0026 -\\frac{n+f}{2} \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\text{平移矩阵 } T} $$当然也可以使用合并后的形式\n$$ M_{\\text{ortho}} = \\begin{bmatrix} \\frac{2}{r-l} \u0026 0 \u0026 0 \u0026 -\\frac{r+l}{r-l} \\\\ 0 \u0026 \\frac{2}{t-b} \u0026 0 \u0026 -\\frac{t+b}{t-b} \\\\ 0 \u0026 0 \u0026 \\frac{2}{n-f} \u0026 -\\frac{n+f}{n-f} \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$可以看到公式中需要左右边界l , r,上下边界 t , b,远近裁剪面 n , f 的值 由于相机是从-z方向看过去的(n\u0026gt;f),所以n需要取负号,所以需要 n \u0026gt; f ,观察main函数发现传入的值是正数,需要取负号.\nn = -zNear; f = -zFar;\n此时缺少 l , r , t , b,观察到该函数中有传入视角 eye_fov 和 ,根据如下公式可以求出 半高t\n$$t = |n| \\cdot \\tan\\left(\\frac{\\text{fovY}}{2}\\right)$$ 接下来求 l , r,已知宽高比 aspect_ratio 和 半高 t 可以由如下公式得到半宽 r :\n$$ r = t \\cdot \\text{aspect} $$ 由于经过视图变换，摄像机在原点，b=-t, l= -r 至此全部需要的数值都得到了,此时代入透视投影矩阵的公式.\n// 已知条件: 视角 eye_fov 宽高比 aspect_ratio ,z近点 zNear z远点 zFar // 思路 因为垂直方向半高 t = |z| * tan(fov/2) 所以可以得到半高t , // 已知宽高比aspect_ratio和半高 t 可以得到水平方向半宽r = t * aspect_ratio Eigen::Matrix4f projection = Eigen::Matrix4f::Identity(); // 垂直方向半高 t // numext 接收的是弧度值,需要将角度(eye_fov)转为弧度 // 把角度转成弧度 // 前面求出来t和r ,将其映射到一个[-1,1]3的坐标系中,可得出: // 左面 l = -r , 右面 r = r // 顶面 t = t , 地面 b = -t // 近平面 n = zNear ,远平面 f = zFar // 由于相机是从-z方向看过去的(n\u0026gt;f),所以n需要取负号 // 可以写出正交投影矩阵 ortho float l, b, n, f; n = -zNear; f = -zFar; float rad = eye_fov * (MY_PI / 180.0); float t = abs(n) * numext::tan(rad / 2); // 水平方向半宽 r float r = t * aspect_ratio; l = -r; b = -t; // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation Eigen::Matrix4f scale; // 旋转矩阵 scale scale \u0026lt;\u0026lt; 2 / (r - l), 0, 0, 0, // 1 0, 2 / (t - b), 0, 0, // 2 0, 0, 2 / (n - f), 0, // 3 0, 0, 0, 1; Eigen::Matrix4f translation; translation \u0026lt;\u0026lt; 1, 0, 0, -(l + r) / 2, // 1 0, 1, 0, -(t + b) / 2, // 2 0, 0, 1, -(n + f) / 2, // 3 0, 0, 0, 1; // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation Eigen::Matrix4f ortho = scale * translation; //投影变换矩阵 persp_to_ortho Eigen::Matrix4f persp_to_ortho; persp_to_ortho \u0026lt;\u0026lt; n, 0, 0, 0, // 1 0, n, 0, 0, // 2 0, 0, n + f, -(n * f), // 3 0, 0, 1, 0; projection = ortho * persp_to_ortho; 函数完整代码\nEigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar) { // 学生将实现此函数 // 已知条件: 视角 eye_fov 宽高比 aspect_ratio ,z近点 zNear z远点 zFar // 思路 因为垂直方向半高 t = |z| * tan(fov/2) 所以可以得到半高t , // 已知宽高比aspect_ratio和半高 t 可以得到水平方向半宽r = t * aspect_ratio Eigen::Matrix4f projection = Eigen::Matrix4f::Identity(); // 垂直方向半高 t // numext 接收的是弧度值,需要将角度(eye_fov)转为弧度 // 把角度转成弧度 // 前面求出来t和r ,将其映射到一个[-1,1]3的坐标系中,可得出: // 左面 l = -r , 右面 r = r // 顶面 t = t , 地面 b = -t // 近平面 n = zNear ,远平面 f = zFar // 由于相机是从-z方向看过去的(n\u0026gt;f),所以n需要取负号 // 可以写出正交投影矩阵 ortho float l, b, n, f; n = -zNear; f = -zFar; float rad = eye_fov * (MY_PI / 180.0); float t = abs(n) * numext::tan(rad / 2); // 水平方向半宽 r float r = t * aspect_ratio; l = -r; b = -t; // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation Eigen::Matrix4f scale; // 旋转矩阵 scale scale \u0026lt;\u0026lt; 2 / (r - l), 0, 0, 0, // 1 0, 2 / (t - b), 0, 0, // 2 0, 0, 2 / (n - f), 0, // 3 0, 0, 0, 1; Eigen::Matrix4f translation; translation \u0026lt;\u0026lt; 1, 0, 0, -(l + r) / 2, // 1 0, 1, 0, -(t + b) / 2, // 2 0, 0, 1, -(n + f) / 2, // 3 0, 0, 0, 1; // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation Eigen::Matrix4f ortho = scale * translation; //投影变换矩阵 persp_to_ortho Eigen::Matrix4f persp_to_ortho; persp_to_ortho \u0026lt;\u0026lt; n, 0, 0, 0, // 1 0, n, 0, 0, // 2 0, 0, n + f, -(n * f), // 3 0, 0, 1, 0; projection = ortho * persp_to_ortho; // TODO: 实现此函数 // 根据给定参数创建投影矩阵。 // 然后返回该矩阵。 return projection; } 提高题 # 在 main.cpp 中构造一个函数Eigen::Matrix4f get_rotation(Vector3f axis, float angle)，该函数的作用是得到绕任意过原点的轴的旋转变换矩阵。 任意过原点的轴旋转可以使用之前提到的罗德里格斯旋转公式. $$ \\mathbf{R}(\\mathbf{n}, \\alpha) = \\cos(\\alpha) \\mathbf{I} + (1 - \\cos(\\alpha)) \\mathbf{n} \\mathbf{n}^T + \\sin(\\alpha) \\underbrace{ \\begin{pmatrix} 0 \u0026 -n_z \u0026 n_y \\\\ n_z \u0026 0 \u0026 -n_x \\\\ -n_y \u0026 n_x \u0026 0 \\end{pmatrix} }_{\\mathbf{N}} $$ // 使用 罗德里格旋转公式的矩阵形式 进行求解 float rad_a = angle * (MY_PI / 180.0); Eigen::Vector4f n; // 单位旋转轴向量 n,使用齐次坐标 Eigen::Matrix4f I; // 3x3 单位矩阵 I Eigen::RowVector4f n_t; // 单位轴的外积矩阵 nn_t Eigen::Matrix4f N; // n 的反对称矩阵 N n \u0026lt;\u0026lt; axis.x(), axis.y(), axis.z(), 0; n_t \u0026lt;\u0026lt; axis.x(), axis.y(), axis.z(), 0; I = Eigen::Matrix4f::Identity(); // 生成单位矩阵 I axis.normalize(); // 提取n的三个分量 float nx = n.x(); float ny = n.y(); float nz = n.z(); // 根据反矩阵公式写出 N ; N \u0026lt;\u0026lt; 0, -nz, ny, 0, // 1 nz, 0, -nx, 0, // 2 -ny, nx, 0, 0, // 3 0, 0, 0, 1; //第4行：齐次坐标规范（最后一位1） // 最终的旋转矩阵 R_na Eigen::Matrix4f R_na = numext::cos(rad_a) * I + (1 - numext::cos(rad_a)) * (n * n_t) + numext::sin(rad_a) * N; 做完看别人的视频对答案发现我的三角形比例不对旋转的时候会变大,问了下ai得知: 旋转矩阵由罗德里格斯公式计算，但在代码中被错误地扩展到了4x4且定义了错误的 N 矩阵。 最后导致代码中: $$ rotation(3,3) = \\cos\\alpha + 0 + \\sin\\alpha = \\cos\\alpha + \\sin\\alpha $$ 会破坏齐次坐标的规范性。所以需要将矩阵右下角改为1\n完整函数代码\n//[提高项 5 分] 在 main.cpp //中构造一个函数，该函数的作用是得到绕任意过原点的轴的旋转变换矩阵。 // Eigen::Matrix4f get_rotation(Vector3f axis, float angle) Eigen::Matrix4f get_rotation(Vector3f axis, float angle) { // 使用 罗德里格旋转公式的矩阵形式 进行求解 float rad_a = angle * (MY_PI / 180.0); Eigen::Vector4f n; // 单位旋转轴向量 n,使用齐次坐标 Eigen::Matrix4f I; // 3x3 单位矩阵 I Eigen::RowVector4f n_t; // 单位轴的外积矩阵 nn_t Eigen::Matrix4f N; // n 的反对称矩阵 N n \u0026lt;\u0026lt; axis.x(), axis.y(), axis.z(), 0; n_t \u0026lt;\u0026lt; axis.x(), axis.y(), axis.z(), 0; I = Eigen::Matrix4f::Identity(); // 生成单位矩阵 I axis.normalize(); // 提取n的三个分量 float nx = n.x(); float ny = n.y(); float nz = n.z(); // 根据反矩阵公式写出 N ; N \u0026lt;\u0026lt; 0, -nz, ny, 0, // 1 nz, 0, -nx, 0, // 2 -ny, nx, 0, 0, // 3 0, 0, 0, 1; //第4行：齐次坐标规范（最后一位1） // 最终的旋转矩阵 R_na Eigen::Matrix4f R_na = numext::cos(rad_a) * I + (1 - numext::cos(rad_a)) * (n * n_t) + numext::sin(rad_a) * N; R_na(3, 3) = 1.0f; // 强制齐次矩阵最后一位为1（防止浮点误差） return R_na; } 全部代码 # #include \u0026#34;Triangle.hpp\u0026#34; #include \u0026#34;rasterizer.hpp\u0026#34; #include \u0026lt;cmath\u0026gt; #include \u0026lt;eigen3/Eigen/Eigen\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;opencv2/opencv.hpp\u0026gt; constexpr double MY_PI = 3.1415926; // 相机视角矩阵 Eigen::Matrix4f get_view_matrix(Eigen::Vector3f eye_pos) { Eigen::Matrix4f view = Eigen::Matrix4f::Identity(); Eigen::Matrix4f translate; translate \u0026lt;\u0026lt; 1, 0, 0, -eye_pos[0], 0, 1, 0, -eye_pos[1], 0, 0, 1, -eye_pos[2], 0, 0, 0, 1; // 齐次坐标 view = translate * view; return view; } //模型矩阵 参数是一个旋转的角度 Eigen::Matrix4f get_model_matrix(float rotation_angle) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); // TODO: 实现此函数 // 创建绕 Z 轴旋转三角形的模型矩阵。 // 然后返回该矩阵。 float rad = rotation_angle * (MY_PI / 180); Eigen::Matrix4f rotation; // 旋转矩阵中numext::sin的三角函数接收的是弧度需要将角度转为弧度:弧度=角度*(PI/180) rotation \u0026lt;\u0026lt; numext::cos(rad), -numext::sin(rad), 0, 0, // 第一行 numext::sin(rad), numext::cos(rad), 0, 0, // 第二行 0, 0, 1, 0, //第三行 0, 0, 0, 1; model = rotation * model; return model; } Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar) { // 学生将实现此函数 // 已知条件: 视角 eye_fov 宽高比 aspect_ratio ,z近点 zNear z远点 zFar // 思路 因为垂直方向半高 t = |z| * tan(fov/2) 所以可以得到半高t , // 已知宽高比aspect_ratio和半高 t 可以得到水平方向半宽r = t * aspect_ratio Eigen::Matrix4f projection = Eigen::Matrix4f::Identity(); // 垂直方向半高 t // numext 接收的是弧度值,需要将角度(eye_fov)转为弧度 // 把角度转成弧度 // 前面求出来t和r ,将其映射到一个[-1,1]3的坐标系中,可得出: // 左面 l = -r , 右面 r = r // 顶面 t = t , 地面 b = -t // 近平面 n = zNear ,远平面 f = zFar // 由于相机是从-z方向看过去的(n\u0026gt;f),所以n需要取负号 // 可以写出正交投影矩阵 ortho float l, b, n, f; n = -zNear; f = -zFar; float rad = eye_fov * (MY_PI / 180.0); float t = abs(n) * numext::tan(rad / 2); // 水平方向半宽 r float r = t * aspect_ratio; l = -r; b = -t; // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation Eigen::Matrix4f scale; // 旋转矩阵 scale scale \u0026lt;\u0026lt; 2 / (r - l), 0, 0, 0, // 1 0, 2 / (t - b), 0, 0, // 2 0, 0, 2 / (n - f), 0, // 3 0, 0, 0, 1; Eigen::Matrix4f translation; translation \u0026lt;\u0026lt; 1, 0, 0, -(l + r) / 2, // 1 0, 1, 0, -(t + b) / 2, // 2 0, 0, 1, -(n + f) / 2, // 3 0, 0, 0, 1; // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation Eigen::Matrix4f ortho = scale * translation; //投影变换矩阵 persp_to_ortho Eigen::Matrix4f persp_to_ortho; persp_to_ortho \u0026lt;\u0026lt; n, 0, 0, 0, // 1 0, n, 0, 0, // 2 0, 0, n + f, -(n * f), // 3 0, 0, 1, 0; projection = ortho * persp_to_ortho; // TODO: 实现此函数 // 根据给定参数创建投影矩阵。 // 然后返回该矩阵。 return projection; } //[提高项] 在 main.cpp //中构造一个函数，该函数的作用是得到绕任意过原点的轴的旋转变换矩阵。 // Eigen::Matrix4f get_rotation(Vector3f axis, float angle) Eigen::Matrix4f get_rotation(Vector3f axis, float angle) { // 使用 罗德里格旋转公式的矩阵形式 进行求解 float rad_a = angle * (MY_PI / 180.0); Eigen::Vector4f n; // 单位旋转轴向量 n,使用齐次坐标 Eigen::Matrix4f I; // 3x3 单位矩阵 I Eigen::RowVector4f n_t; // 单位轴的外积矩阵 n_t Eigen::Matrix4f N; // n 的反对称矩阵 N n \u0026lt;\u0026lt; axis.x(), axis.y(), axis.z(), 0; n_t \u0026lt;\u0026lt; axis.x(), axis.y(), axis.z(), 0; I = Eigen::Matrix4f::Identity(); // 生成单位矩阵 I axis.normalize(); // 将传入的旋转轴归一化 // 提取n的三个分量 float nx = n.x(); float ny = n.y(); float nz = n.z(); // 根据反矩阵公式写出 N ; N \u0026lt;\u0026lt; 0, -nz, ny, 0, // 1 nz, 0, -nx, 0, // 2 -ny, nx, 0, 0, // 3 0, 0, 0, 1; //第4行：齐次坐标规范（最后一位1） // 最终的旋转矩阵 R_na Eigen::Matrix4f R_na = numext::cos(rad_a) * I + (1 - numext::cos(rad_a)) * (n * n_t) + numext::sin(rad_a) * N; R_na(3, 3) = 1.0f; // 强制齐次矩阵最后一位为1（防止浮点误差） return R_na; } int main(int argc, const char **argv) { float angle = 0; bool command_line = false; float rangle = 0; // 1. 定义多组可选旋转轴 std::vector\u0026lt;Eigen::Vector3f\u0026gt; axes = { Eigen::Vector3f(1, 0, 0), // X轴 Eigen::Vector3f(0, 1, 0), // Y轴 Eigen::Vector3f(0, 0, 1) // Z轴 }; // 2. 初始化当前轴索引（默认用Z轴） int axis_index = 2; Vector3f axis = axes[axis_index]; std::string filename = \u0026#34;output.png\u0026#34;; if (argc \u0026gt;= 3) { command_line = true; angle = std::stof(argv[2]); // -r by default if (argc == 4) { filename = std::string(argv[3]); } else return 0; } rst::rasterizer r(700, 700); Eigen::Vector3f eye_pos = {0, 0, 5}; // 相机视角z轴距离 5 std::vector\u0026lt;Eigen::Vector3f\u0026gt; pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}}; std::vector\u0026lt;Eigen::Vector3i\u0026gt; ind{{0, 1, 2}}; auto pos_id = r.load_positions(pos); auto ind_id = r.load_indices(ind); int key = 0; int frame_count = 0; if (command_line) { r.clear(rst::Buffers::Color | rst::Buffers::Depth); r.set_model(get_model_matrix(angle)); r.set_view(get_view_matrix(eye_pos)); r.set_projection(get_projection_matrix(45, 1, 0.1, 50)); r.draw(pos_id, ind_id, rst::Primitive::Triangle); cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data()); image.convertTo(image, CV_8UC3, 1.0f); cv::imwrite(filename, image); return 0; } while (key != 27) { // 27是ESC键的ASCII码，按ESC退出循环 r.clear(rst::Buffers::Color | rst::Buffers::Depth); // Eigen 库中矩阵乘法是左乘，需保证变换顺序（模型→视图→投影） Eigen::Matrix4f model = get_rotation(axis, angle) * get_model_matrix(angle); r.set_model(model); r.set_view(get_view_matrix(eye_pos)); r.set_projection(get_projection_matrix(45, 1, 0.1, 50)); r.draw(pos_id, ind_id, rst::Primitive::Triangle); cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data()); image.convertTo(image, CV_8UC3, 1.0f); cv::imshow(\u0026#34;image\u0026#34;, image); key = cv::waitKey(10); std::cout \u0026lt;\u0026lt; \u0026#34;frame count: \u0026#34; \u0026lt;\u0026lt; frame_count++ \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; if (key == \u0026#39;a\u0026#39;) { angle += 10; } else if (key == \u0026#39;d\u0026#39;) { angle -= 10; } else if (key == \u0026#39;r\u0026#39;) { // 循环切换轴索引（0→1→2→0...） axis_index = (axis_index + 1) % axes.size(); // 更新当前旋转轴 axis = axes[axis_index]; // 打印提示，明确当前轴 std::cout \u0026lt;\u0026lt; \u0026#34;旋转轴已切换为：\u0026#34;; if (axis_index == 0) std::cout \u0026lt;\u0026lt; \u0026#34;X 轴 (1,0,0)\u0026#34;; else if (axis_index == 1) std::cout \u0026lt;\u0026lt; \u0026#34;Y 轴 (0,1,0)\u0026#34;; else if (axis_index == 2) std::cout \u0026lt;\u0026lt; \u0026#34;Z 轴 (0,0,1)\u0026#34;; std::cout \u0026lt;\u0026lt; std::endl; } } } 生成结果:\n","date":"2026 年 03 月 15 日","externalUrl":null,"permalink":"/posts/games101_hw1/","section":"Posts","summary":"","title":"GAMES101作业1 含提高","type":"posts"},{"content":"真正开始频繁喝咖啡,是从上班以后.\n最开始喝的，是那种带植脂末的速溶咖啡，甜、香、顺口，也不用讲究什么，就是为了提神，撑过上班的日子。喝久了，慢慢就不再喜欢那股甜腻的味道，开始转向纯黑咖啡。一开始觉得苦，喝习惯了，反而不觉得难入口，注意力也更多放在它提神的效果上。\n后来又尝试了挂耳、冻干这些。喝挂耳的时候，试过浅烘焙，酸度太高，实在接受不了，总觉得太尖锐。听说豆汁也是酸的，不知道是不是这种感觉，反正我不太喜欢。于是就固定喝深度烘焙。\n深烘的咖啡是苦，但这种苦和浓茶不一样。咖啡的苦来得直接，一下就过去；浓茶的苦更柔，更绵长。两者完全是两种感觉。\n我现在常喝的，就是深度烘焙的挂耳。冲开的时候咖啡香气很足，味道我也喜欢，唯一一点就是，论提神效果，还是冻干咖啡更猛一些。\n其实现在再喝黑咖啡，已经完全不觉得有多难接受了。\n想起上学的时候，别人给过我一包云南黑咖啡，那时候第一次喝这么纯的黑咖啡，只觉得苦到受不了，泡好之后喝一口就直接吐了，完全喝不下去。\n可现在上班久了，天天喝黑咖啡、深烘挂耳，不加糖也不加奶，再回头去想当年那种苦，只觉得：也就那样吧。\n苦，也就苦那么一下子。\n明明是同一种 “苦”，小时候喝不下，现在却习以为常。\n想来，这大概也是一种慢慢长大、慢慢扛事的心态变化吧。\n从甜腻的速溶，到纯粹的深烘，\n口味在变，人也在悄悄变。\n咖啡喝的不只是味道，还有那段慢慢适应、慢慢习惯的时光。\n","date":"2026 年 03 月 14 日","externalUrl":null,"permalink":"/posts/%E4%BB%8E%E4%B8%80%E6%9D%AF%E5%92%96%E5%95%A1%E8%AF%B4%E5%BC%80%E5%8E%BB/","section":"Posts","summary":"","title":"从一杯咖啡说开去","type":"posts"},{"content":"","date":"2026 年 03 月 14 日","externalUrl":null,"permalink":"/categories/%E9%9A%8F%E7%AC%94/","section":"Categories","summary":"","title":"随笔","type":"categories"},{"content":"","date":"2026 年 03 月 14 日","externalUrl":null,"permalink":"/tags/%E9%9A%8F%E7%AC%94/","section":"Tags","summary":"","title":"随笔","type":"tags"},{"content":" 声明：部分内容参考网上博客。\n教程地址: # GAMES101-现代计算机图形学入门-闫令琪\n课程主页: # GAMES101: 现代计算机图形学入门\n作业汇总: http://games-cn.org/forums/topic/allhw/\n一些笔记参考: TA学习笔记\n三角形网格(Triangle Meshes) # 三角形具有的性质:\n三角形是最简单的多边形（3条边、3个顶点），无法再简化 任何复杂的多边形（如四边形、五边形甚至不规则多边形）都可以通过“三角剖分”（triangulation）算法分解成若干个三角形 任意三个不共线的点必然确定一个平面 → 三角形一定是平的 三角形没有凹角、自交等问题，其“内部”区域清晰无歧义 定义三个顶点后，三角形内可以插值 光栅化(Rasterization) # 光栅化的关键作用: 哦安短一个像素和三角形的位置关系（像素中心点与三角形的位置关系）\n采样(Sampling) # 采样的过程是把一个函数离散化的过程\nfor(int x = 0; x \u0026lt; xmax; ++x) output[x] = f(x); 光栅化采样：利用像素中心对屏幕空间进行采样。\n光栅化采样的目的：判断像素中心是否在三角形内 引用下AI举的例子:\n光栅化采样 · 生活比喻（绣花版）\n💬 用奶奶熟悉的十字绣，解释电脑怎么把光滑图形画到方格屏幕上。\n🌸 场景设定\n原始图形：一张光滑圆润的牡丹花样子（连续、无锯齿）。 实际画布：一块格子布（像十字绣布），由一个个小方块组成。 限制：只能在整格里下针，不能绣半格、斜线或曲线。 🔍 核心规则： 看“格子中心” 对每一个小格子，只检查它的正中心点： ✅ 如果花的轮廓盖住了中心点 → 在这个格子里绣一针。 ❌ 如果没盖住 → 不绣，留空。\n这个“看中心点”的判断动作，就是 采样（Sampling）。\n🖼️ 结果 - 绣出来的花是由方格组成的，边缘有“小台阶”（锯齿）。\n但站远一点看，依然是一朵完整的花！ ——这和电脑在屏幕上画图完全一样。 🧠 对应到计算机图形学\n光滑的花样子 \u0026ndash;\u0026gt; 3D 几何图元（如三角形）\n格子布 \u0026ndash;\u0026gt; 屏幕像素网格\n每个格子的中心\u0026ndash;\u0026gt;像素采样点\n“盖住中心就绣” \u0026ndash;\u0026gt;光栅化采样规则\n绣出的花\u0026ndash;\u0026gt;光栅化后的图像（帧缓冲)\n💡 一句话总结 光栅化采样 = 电脑“绣花”——看每个像素的“心”有没有被图形盖住，盖住了就点亮它！\n$$ \\text{inside}(t, x, y) = \\begin{cases} 1 \u0026 \\text{Point } (x, y) \\text{ in triangle } t \\\\ 0 \u0026 \\text{otherwise} \\end{cases} $$// 从屏幕最左边（x=0）开始，一直扫描到最右边（x = xmax - 1） for (int x = 0; x \u0026lt; xmax; ++x) // 在当前这一列（x 固定），从屏幕最上边（y=0）扫到最下边（y = ymax - 1） for (int y = 0; y \u0026lt; ymax; ++y) // 判断当前像素是否属于三角形： // - 每个像素是一个小方块，它的中心点坐标是 (x + 0.5, y + 0.5) // - 调用 inside(tri, x+0.5, y+0.5) 来检查这个中心点是否在三角形 tri 内部 // - 如果在，inside 返回 1（点亮像素）；否则返回 0（不点亮） image[x][y] = inside(tri, x + 0.5, y + 0.5); 判断像素是否在三角形内 # 使用 叉乘 来判断点是否在三角形内部\n三角形内外判断 一、准备工作：定义向量\n我们以图中的三角形 ABC 和内部点 P 为例。\n首先，给三角形的每条边定义一个有向边向量，并定义从边起点到 P 点的向量：\n边 边向量 点向量（从边起点到 P） AB AB=B−A AP=P−A BC BC=C−B BP=P−B CA CA=A−C CP=P−C 二、核心计算：三次叉乘\n对每条边，计算 “边向量” 与 “点向量” 的叉乘。\n我们在 x-y 平面进行计算，叉乘结果只有 z 分量，我们只需关注其正负号。\n第一条边 AB\nc1​=AB×AP\n图中 P 在 AB 的左侧，所以 c1​\u0026gt;0\n第二条边 BC\nc2​=BC×BP\n图中 P 在 BC 的左侧，所以 c2​\u0026gt;0\n第三条边 CA\nc3​=CA×CP\n图中 P 在 CA 的左侧，所以 c3​\u0026gt;0\n三、判断内外：符号一致性\n内部：c1​,c2​,c3​ 全部同号（全部为正，或全部为负）\n就像图中的 P 点，三个叉乘结果都为正，说明它始终在所有边的同一侧。\n外部：如果有任何一个结果的符号与其他不同\n说明点 P 穿过了至少一条边。 四、一句话记忆\n所有叉乘结果同号 → 内部；出现异号 → 外部\n三角形ABC\nAB×AP , BC×BP , CA×CP 符号(方向) 相同则点P在三角形内部,否则在外部（叉乘反映向量的左右位置关系)\n特殊情况：边上的点\n不做处理：本课程 特殊处理：OpenGL/DirectX 优化—— 使用包围盒(Bounding Box)进行优化 # 因此自然的，只需要遍历每一个点就可以得出三角形的光栅化结果了！\n优化：因为显然并没有必要去测试屏幕中的每一个点，一个三角形面可能只占屏幕很小的部分，可以利用一个bouding box包围住想要测试的三角形，只对该bounding box内的点进行采样测试，如下图:\n黄色三角形：要渲染的图元 蓝色区域：包围盒（Bounding Box） 红色点：在三角形内的像素（会被点亮） 黑色点：在三角形外的像素（忽略） 只检查包围盒内的像素，避免对整个屏幕进行无效判断。\n优点\n✅ 缩小搜索范围 ✅ 提高渲染速度 ✅ 减少不必要的计算 缩写:AABB (Axis-Aligned Bounding Box)\nBounding Box（包围盒）：就是一个矩形框，把一个图形刚好包住，不多不少。 Axis-Aligned（轴对齐）：意思是这个框的边必须跟屏幕的上下左右对齐 特点: 边与屏幕对齐，计算超快\n锯齿 # 屏幕由离散像素组成，无法精确表示连续曲线 → 导致边缘出现“楼梯状”锯齿。\n在经过上述的光栅化过程后，会得到如下图片，称为锯齿或走样。\n而反锯齿或反走样是图形学一大挑战。\n","date":"2026 年 03 月 07 日","externalUrl":null,"permalink":"/posts/games101_05/","section":"Posts","summary":"","title":"GAMES101学习笔记05:光栅化(三角形的离散化)","type":"posts"},{"content":" 参考的一些笔记\nLecture 04 - Transformation Cont. TA学习笔记 CS自学指南_GAMES101\nMVP变换（Model-View-Projection Matrices） # MVP变换用来描述视图变换的任务，即将虚拟世界中的三维物体映射（变换）到二维坐标中。\nMVP变换分为三步：\n模型变换(model tranformation)：将模型空间转换到世界空间（找个好的地方，把所有人集合在一起，摆个pose） 摄像机变换(view tranformation)：将世界空间转换到观察空间（找到一个放相机的位置，往某一个角度去看） 投影变换(projection tranformation)：将观察空间转换到裁剪空间（茄子！） 视图变换（View） # 视图变换的目的是变换Camera位置到原点，上方为Y，观察方向为-Z Camera的y轴正方向向上，z轴方向是\\(\\vec{x} \\times \\vec{y}\\)右手系）\n对物体进行运动，摄像机会跟随着一起运动保持相对位置不变\n怎么写出数学表示\n通过\\(V_{\\text{view}}\\)​变换相机，在数学上怎么表示？\n将e平移到零点 旋转g到−Z 旋转t到Y 旋转G x T到X 可以将其拆解成两个部分: 平移+旋转 $$M_{VIEW} = R_{VIEW} T_{VIEW}$$首先将相机从e平移到标准原点的位置\n$$ T_{VIEW} = \\begin{bmatrix} 1 \u0026 0 \u0026 0 \u0026 -x_e \\\\ 0 \u0026 1 \u0026 0 \u0026 -y_e \\\\ 0 \u0026 0 \u0026 1 \u0026 -z_e \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$由于旋转矩阵具有正交性，因此它的逆矩阵就等于它的转置矩阵.\n$$ \\begin{bmatrix} 1 \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} \\cdot \\begin{bmatrix} x_{\\hat{g}\\times\\hat{t}} \u0026 x_t \u0026 x_{-g} \u0026 0 \\\\ y_{\\hat{g}\\times\\hat{t}} \u0026 y_t \u0026 y_{-g} \u0026 0 \\\\ z_{\\hat{g}\\times\\hat{t}} \u0026 z_t \u0026 z_{-g} \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} = E \\cdot \\begin{bmatrix} x_{\\hat{g}\\times\\hat{t}} \u0026 x_t \u0026 x_{-g} \u0026 0 \\\\ y_{\\hat{g}\\times\\hat{t}} \u0026 y_t \u0026 y_{-g} \u0026 0 \\\\ z_{\\hat{g}\\times\\hat{t}} \u0026 z_t \u0026 z_{-g} \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$在通过正交矩阵的性质(正交矩阵的逆等于它的转置矩阵),可以求出:\n$$ R_{\\text{view}} = \\begin{bmatrix} x_{\\hat{g} \\times \\hat{t}} \u0026 y_{\\hat{g} \\times \\hat{t}} \u0026 z_{\\hat{g} \\times \\hat{t}} \u0026 0 \\\\ x_t \u0026 y_t \u0026 z_t \u0026 0 \\\\ x_{-g} \u0026 y_{-g} \u0026 z_{-g} \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$ 总结：\n让物体和相机一起变换。 直到相机位于原点，头朝上（Y），看向 −Z 投影变换（Projection） # 投影变换分为两种：\n正交投影(Orthogonal projection)变换：透视线平行 透视投影(Perspective projection)变换：透视线相交，近大远小 简单来讲，透视投影相当于两点透视或三点透视，有透视畸变。正交投影相当于是基于坐标的轴测图\n正交投影 (Orthogonal projection) # 一种简单的理解方式\n相机位于原点，朝向 -Z 方向，向上为 Y 方向\n丢弃 Z 坐标\n将得到的矩形平移并缩放到 [-1, 1]² 范围内 我们希望将一个长方体区域 [l, r] × [b, t] × [f, n] (左右 X 下上 X 远近)\n映射到立方体 [-1, 1]³\n方法:\n现将标准立方体拉到原点 后将x，y，z轴各自**伸缩到[−1,1] 变换顺序：先移动（中点移动到原点），再缩放(基向量缩放比例为\\(\\frac{2}{\\text{长} / \\text{宽} / \\text{高}}\\)\n因为相机朝向-z方向(右手系)，此时n \u0026gt; f，这也是 OpenGL 用左手系( f \u0026gt; n )的原因。\n正交投影矩阵（Orthographic Projection Matrix) # $$ M_{\\text{ortho}} = \\underbrace{ \\begin{bmatrix} \\frac{2}{r-l} \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 \\frac{2}{t-b} \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 \\frac{2}{n-f} \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\text{缩放矩阵 } S} \\cdot \\underbrace{ \\begin{bmatrix} 1 \u0026 0 \u0026 0 \u0026 -\\frac{r+l}{2} \\\\ 0 \u0026 1 \u0026 0 \u0026 -\\frac{t+b}{2} \\\\ 0 \u0026 0 \u0026 1 \u0026 -\\frac{n+f}{2} \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\text{平移矩阵 } T} $$合并后形式\n$$ M_{\\text{ortho}} = \\begin{bmatrix} \\frac{2}{r-l} \u0026 0 \u0026 0 \u0026 -\\frac{r+l}{r-l} \\\\ 0 \u0026 \\frac{2}{t-b} \u0026 0 \u0026 -\\frac{t+b}{t-b} \\\\ 0 \u0026 0 \u0026 \\frac{2}{n-f} \u0026 -\\frac{n+f}{n-f} \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$参数说明：\nl, r → 左右边界（left / right） b, t → 下上边界（bottom / top） f, n → 远近裁剪面（far / near），注意在右手系中通常 n \u0026gt; f （如 n = -0.1, f = -100） 透视投影 (Perspective projection) # 透视投影是应用最广泛的投影。 近大远小。 平行线看上去不再平行。 回顾：齐次坐标的性质\n(x, y, z, 1)、(kx, ky, kz, k ≠ 0)、(xz, yz, z², z ≠ 0) 都表示三维空间中的同一个点 (x, y, z) 例如：(1, 0, 0, 1) 和 (2, 0, 0, 2) 都表示点 (1, 0, 0) 简单，但很有用\n如何做透视投影 # 更好理解的方式是想象原屏幕上的四个顶点，我们把他们挤到和近平面同一个高度上。换言之，我们把他“挤”成了空间中的一个长方体。然后做正交投影就行了。\n在挤压的操作中我们规定几点：\n近平面上的点永远不变（想象你要把窗户外面的东西画到窗户上，窗户上的东西不会改变）。 远平面的点z值不会发生变化。 远平面的中心点不会变。 研究挤压：\n从侧面观察挤压过程 根据相似三角形,可以得出x和x' 与 y和y'的对应关系:\n$$ y' = \\frac{n}{z}y, \\quad x' = \\frac{n}{z}x $$原本的坐标根据齐次坐标表示会变成（第二步同乘以 z） $$ \\begin{bmatrix} x \\\\ y \\\\ z \\\\ 1 \\end{bmatrix} \\rightarrow \\begin{bmatrix} nx/z \\\\ ny/z \\\\ \\text{unknown} \\\\ 1 \\end{bmatrix} \\overset{\\text{mult. by } z}{==}\\begin{bmatrix} nx \\\\ ny \\\\ z \\cdot \\text{unknown} \\\\ z \\end{bmatrix} $$把这个变换用齐次坐标矩阵表示：\n$$ M_{(4 \\times 4)} \\begin{bmatrix} x \\\\ y \\\\ z \\\\ 1 \\end{bmatrix} == \\begin{bmatrix} nx \\\\ ny \\\\ z \\cdot \\text{unknown} \\\\ z \\end{bmatrix} $$根据矩阵乘法规则，我们可以反推出矩阵 M 的结构如下：\n$$ M_{persp \\to ortho} = \\begin{pmatrix} n \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 n \u0026 0 \u0026 0 \\\\ ? \u0026 ? \u0026 ? \u0026 ? \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\end{pmatrix} $$投影 == 挤压 x y + 正交投影，look at -z 的时候，z 上的坐标被干掉了，所以投影变换前后 z 的坐标不变\n第三行负责计算变换后的 z 坐标（z’）\n近裁剪平面上的任意点，其 z 值不会改变 远裁剪平面上的任意点，其 z 值也不会改变 我们再利用下面两条性质就可以得到未知的部分\n近平面上的点不变 远平面中心点不变 条件一：近平面上的任意点(令unknown=n,z=n): $$ M \\cdot P_{near} = \\begin{pmatrix} n \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 n \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 A \u0026 B \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\end{pmatrix} \\begin{bmatrix} x \\\\ y \\\\ n \\\\ 1 \\end{bmatrix} = \\begin{bmatrix} nx \\\\ ny \\\\ An + B \\\\ n \\end{bmatrix} $$ 因为不涉及旋转，所以第三行与x,y无关。\n$$ \\text{第三行运算结果：} \\quad \\begin{bmatrix} 0 \u0026 0 \u0026 A \u0026 B \\end{bmatrix} \\cdot \\begin{bmatrix} x \\\\ y \\\\ n \\\\ 1 \\end{bmatrix} = n^2 $$ 即:\n$$ An + B = n^2 \\tag{1} $$ 条件二：远平面中心点不变 远平面的中心点满足 x=0,y=0,z=f: $$ P_{far\\_center} = \\begin{bmatrix} 0 \\\\ 0 \\\\ f \\\\ 1 \\end{bmatrix} $$将其乘以矩阵 M ，关注结果向量的第三分量（即变换后的 z′）：\n$$ M \\cdot P_{far\\_center} = \\begin{pmatrix} n \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 n \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 A \u0026 B \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\end{pmatrix} \\begin{bmatrix} 0 \\\\ 0 \\\\ f \\\\ 1 \\end{bmatrix} = \\begin{bmatrix} 0 \\\\ 0 \\\\ Af + B \\\\ f \\end{bmatrix} $$同理可得: $$ \\text{第三行运算结果：} \\quad \\begin{bmatrix} 0 \u0026 0 \u0026 A \u0026 B \\end{bmatrix} \\cdot \\begin{bmatrix} 0 \\\\ 0 \\\\ f \\\\ 1 \\end{bmatrix} = f^2 $$ 即:\n$$ Af + B = f^2 \\tag{2} $$求解方程组 因此可以得到(1)和 (2)两个关系式,联立方程 (1) 和 (2)：:\n$$\\begin{cases} An + B = n^2 \\\\ Af + B = f^2 \\end{cases}$$ 第一步：消元求解 A 用方程 (2) 减去方程 (1)： $$A(f - n) = f^2 - n^2$$利用平方差公式展开右边：\n$$A(f - n) = (f - n)(f + n)$$解得 A ：\n$$ A = f + n $$ 第二步：代入求解 B 将 A=f+n 代入方程 (1)： $$(f + n)n + B = n^2$$展开并化简： $$fn + n^2 + B = n^2$$解得 B： $$B = -fn$$最终得到:\n$$ \\begin{cases} A = n + f \\\\ B = -nf \\end{cases} $$求得变换矩阵为:\n$$ M_{\\text{persp} \\to \\text{ortho}} = \\begin{bmatrix} n \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 n \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 n + f \u0026 -nf \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\end{bmatrix} $$ 透视投影数学表示和总结 # 总结归纳下:\n透视投影的本质可以总结为 “先变形，后归一化” 的两步走策略。\n想象你在拍一张照片：\n挤压：先把眼前立体的风景，通过某种魔法压扁成一张平面的底片（此时还没透视感，只是数据变了）。 正交：把这张底片裁剪成标准尺寸。 除法：当你透过镜头看这张底片时，大脑（GPU）自动把它还原成立体的画面，远的就远，近的就近。 🎯 核心逻辑：两步变换\n挤压变形 (Shear \u0026amp; Scale)\n动作：把“金字塔形”的视锥体（近小远大），强行挤压成一个“长方体”。 目的：让原本复杂的透视关系，变成简单的正交关系。 关键操作：利用齐次坐标 w 分量，让 zz值影响 x,yx,y 的缩放（即 x′=x⋅n zx′=x⋅zn​ ），实现“近大远小”的数学基础。 正交投影 (Orthographic)\n动作：对挤压后的长方体进行标准的平移和缩放。 目的：将长方体映射到标准设备坐标空间 (NDC, 即 [−1,1]3[−1,1]3 )。 结果：此时所有物体都变成了正交视图，但深度信息 ( zz ) 已被特殊编码保留。 透视除法 (Perspective Division) —— 隐式步骤\n动作：GPU 自动执行 (x,y,z)/w(x,y,z)/w 。 效果：因为第一步中 ww被设为了 z ，除以 ww 后，远处的点坐标变小，近处的点坐标变大，透视效果正式显现。 投影矩阵的变换 最终透视投影矩阵 = 正交投影矩阵 × 透视转正交矩阵\n$$ M_{\\text{persp}} = M_{\\text{ortho}} \\cdot M_{\\text{persp} \\to \\text{ortho}} $$ 整理后就是: $$ M_{\\text{persp}} = \\underbrace{ \\begin{bmatrix} \\frac{2}{r-l} \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 \\frac{2}{t-b} \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 \\frac{2}{n-f} \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\text{缩放矩阵 } S} \\cdot \\underbrace{ \\begin{bmatrix} 1 \u0026 0 \u0026 0 \u0026 -\\frac{r+l}{2} \\\\ 0 \u0026 1 \u0026 0 \u0026 -\\frac{t+b}{2} \\\\ 0 \u0026 0 \u0026 1 \u0026 -\\frac{n+f}{2} \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\text{平移矩阵 } T} \\cdot \\underbrace{ \\begin{bmatrix} n \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 n \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 n+f \u0026 -nf \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\end{bmatrix} }_{M_{\\text{persp} \\to \\text{ortho}}} $$拆解说明：\n这实际上是说：\n标准正交投影矩阵 M_ortho = 缩放矩阵 S × 平移矩阵 T\n也就是说，作者把原本一个复杂的 M_ortho 拆成了两个更直观的几何操作：\n平移 T：将视景体中心移到原点\n→ 对应平移量：(-(r+l)/2, -(t+b)/2, -(n+f)/2)\n缩放 S：将视景体缩放到 [-1,1]³\n→ 对应缩放因子：(2/(r-l), 2/(t-b), 2/(n-f))\n虽然图中保留了完整分解，但在实际编程中（如 OpenGL），我们通常使用简化后的单矩阵版本：\n$$ M_{\\text{persp}} = \\begin{bmatrix} \\frac{2n}{r-l} \u0026 0 \u0026 \\frac{l+r}{l-r} \u0026 0 \\\\ 0 \u0026 \\frac{2n}{t-b} \u0026 \\frac{b+t}{b-t} \u0026 0 \\\\ 0 \u0026 0 \u0026 \\frac{n+f}{n-f} \u0026 \\frac{2fn}{f-n} \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\end{bmatrix} $$ 视口变换 # 经过上述MVP变换以后,所有物体都在 [-1,1]^3 的cube中,下一步需要将他们花在屏幕上,这个过程就是光栅化\n视锥 # 视锥表示看起来像顶部切割后平行于底部的金字塔的实体形状。这是透视摄像机可以看到和渲染的区域的形状。 定义视锥：\n长宽比 Aspect 垂直的角度 FovY 利用视锥来获取物体的长宽高\n需要注意的是:\n近平面总高度 = 2t 近平面总宽度 = 2r 公式解析\n计算半高度t $$ \\tan\\left(\\frac{\\text{fovY}}{2}\\right) = \\frac{t}{|n|} $$ fovY / 2: 这是垂直视野角的一半。如上图所示，它构成了一个直角三角形的一个锐角。 |n|: 这是近平面距离（near plane distance），即从摄像机原点 (0,0,0) 到近平面的距离。因为通常使用右手坐标系且摄像机朝向-Z轴，所以 n 是一个负数，这里取绝对值 |n| 作为三角形的邻边长度。 t: 这是我们要求的对边长度，即从中心线到近平面上边缘的距离。 利用三角函数里面的正切函数求解(tan = 对边/邻边),可以得到t:\n$$ t = |n| \\cdot \\tan\\left(\\frac{\\text{fovY}}{2}\\right) $$ 现在有了摄像机的近平面距离 |n| 和 垂直视野角 fovY，就可以精确计算出近平面的半高 t\n计算半宽度r $$ \\text{aspect} = \\frac{r}{t} $$ aspect: 这是屏幕或图像的宽高比，通常是已知的（例如 16:9 的屏幕，aspect = 16/9 ≈ 1.778）。 r: 这是我们要计算的，即从中心线到近平面右边缘的距离（半宽）。 t: 这是我们刚刚通过第一个公式计算出来的半高。 利用宽高比定义(宽高比 = 总宽 / 总高)将公式变形: $$ r = t \\cdot \\text{aspect} $$现在算出半高 t 并且知道宽高比 aspect 的情况下，可以计算出近平面的半宽 r\n屏幕（Screen） # 二维数组，数组元素为像素,分辨率 = 数组尺寸 属于光栅显示器,典型的光栅成像设备 光栅（Raster） # “Raster” 在德语中就是 “screen” 的意思。 (将图形画在屏幕上) 光栅化” 就是把几何图形（三角形、线条等）绘制到屏幕上的过程，即把矢量/连续空间的数据转换为离散的像素值。 像素（Pixel \u0026lt;- Picture element） # 像素是一个颜色均匀的小正方形 颜色混合而成（红、绿、蓝） 屏幕空间(Screen space) # 认为屏幕左下角是原点，向右是x，向上是y\n规定:\n像素的索引形式为 (x, y)，其中 x 和 y 均为整数 像素的索引范围是从 (0, 0) 到 (width - 1, height - 1) 像素 (x, y) 的中心位于 (x + 0.5, y + 0.5) 屏幕覆盖的范围是从 (0, 0) 到 (width, height) 视口变换公式 # 做的事情：\n先不考虑z轴，把MVP后处于标准立方体映射到屏幕上。即\n$$ [-1, 1]^2 \\to [0, \\text{width}] \\times [0, \\text{height}] $$$$ M_{\\text{viewport}} = \\begin{bmatrix} \\frac{\\text{width}}{2} \u0026 0 \u0026 0 \u0026 \\frac{\\text{width}}{2} \\\\ 0 \u0026 \\frac{\\text{height}}{2} \u0026 0 \u0026 \\frac{\\text{height}}{2} \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} $$把虚拟世界中的任意物体顶点，最终映射到屏幕像素坐标上: $$ M_{\\text{total}} = M_{\\text{viewport}} \\cdot M_{\\text{projection}} \\cdot M_{\\text{view}} \\cdot M_{\\text{model}} $$注意：矩阵乘法不满足交换律！ 必须按“模型 → 视图 → 投影 → 视口”的顺序相乘\n步骤 矩阵名 作用 输入空间 → 输出空间 1.模型变换 M_model 将物体从本地坐标系移到世界坐标系 模型空间 → 世界空间 2️.视图变换 M_view 将世界坐标系转换为以相机为中心的坐标系 世界空间 → 相机空间 3️.投影变换 M_projection 将 3D 场景投影到 2D 平面，并归一化到 NDC 相机空间 → 裁剪空间 → NDC 4️.视口变换 M_viewport 将 NDC 映射到实际屏幕像素坐标 NDC → 屏幕空间 ","date":"2026 年 02 月 19 日","externalUrl":null,"permalink":"/posts/games101_04/","section":"Posts","summary":"","title":"GAMES101学习笔记04:变换(模型.视图,投影)","type":"posts"},{"content":"","date":"2026 年 02 月 14 日","externalUrl":null,"permalink":"/tags/3blue1brown/","section":"Tags","summary":"","title":"3Blue1Brown","type":"tags"},{"content":"","date":"2026 年 02 月 14 日","externalUrl":null,"permalink":"/tags/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0/","section":"Tags","summary":"","title":"线性代数","type":"tags"},{"content":" 变换 (Transformation) # 变换可以用运动的思路来思考. 我们有一个输入向量，然后经过变换之后，得到一个输出向量。整个过程，可以看作是输入的向量移动到了输出的输出的位置。考虑整个平面上的向量，在经过变换之后，得到了一个最新的位置。 $$ \\text{向量输入} \\begin{bmatrix} 1 \\\\ 2 \\end{bmatrix} \\xrightarrow{L(\\vec{v})} \\text{向量输出} \\begin{bmatrix} 2 \\\\ -3 \\end{bmatrix} $$ 线性变换 (Linear transformation) # 线性变换：输入一个向量输出一个向量。变换后网格保持平行且等距。线性变换是对空间的一种变换操作。\n线性变换的条件 # 线性变换的条件：\n直线在变换后依然是直线。 原点还在原来的位置。 即：保持网格线平行且等距分布的变换\n如何用数值描述线性变换（矩阵向量乘法） # 只需要记录基向量变换后的位置，而标量代表输入向量是不变的\n一些动画参考: 【线性变换动画演示】 【【官方双语/合集】线性代数的本质 - 系列合集】 【精准空降到 04:02】 用向量描述线性变换:\n$$ \\begin{array}{c} \\hat{i} \\to \\begin{bmatrix} a \\\\ b \\end{bmatrix} \\quad \\hat{j} \\to \\begin{bmatrix} c \\\\ d \\end{bmatrix} \\\\[12pt] \\begin{bmatrix} x \\\\ y \\end{bmatrix} \\to x \\begin{bmatrix} a \\\\ b \\end{bmatrix} + y \\begin{bmatrix} c \\\\ d \\end{bmatrix} = \\begin{bmatrix} ax + by \\\\ cx + dy \\end{bmatrix} \\end{array} $$坐标变换：矩阵是一种线性空间变换的描述（矩阵的列向量，是坐标变换后的基向量）。\n线性变换的复合 # 变换的复合：一个变换之后再进行另一个变换，这个新的线性变换通常被成为两个独立变换的”复合变换“。\n描述复合变换的两种方式： 追踪ihat和jhat，用矩阵直接完全描述这个复合变换。 先用一个矩阵描述第一个变换，再用另一个矩阵作用于第一次变换后的结果描述总的变换。 方法一可以直接得到这个复合变换 $$ \\begin{bmatrix} 1 \u0026 -1 \\\\ 1 \u0026 0 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} $$方法二,则需要先旋转在进行剪切:\n$$ \\begin{bmatrix} 1 \u0026 1 \\\\ 0 \u0026 1 \\end{bmatrix} \\left( \\begin{bmatrix} 0 \u0026 -1 \\\\ 1 \u0026 0 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} \\right) \\quad \\text{——矩阵乘法。} $$由于这两种变换相同：\n$$ \\begin{bmatrix} 1 \u0026 -1 \\\\ 1 \u0026 0 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 1 \\\\ 0 \u0026 1 \\end{bmatrix} \\left( \\begin{bmatrix} 0 \u0026 -1 \\\\ 1 \u0026 0 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} \\right) $$ 矩阵乘法 # 几何表达 # 矩阵乘法的几何意义：两个变换相继作用 对于基向量i\n$$ \\overbrace{\\begin{bmatrix} 0 \u0026 2 \\\\ 1 \u0026 0 \\end{bmatrix}}^{M_2} \\overbrace{\\begin{bmatrix} \\color{green}{1} \u0026 -2 \\\\ \\color{green}{1} \u0026 0 \\end{bmatrix}}^{M_1} = \\begin{bmatrix} ? \u0026 ? \\\\ ? \u0026 ? \\end{bmatrix} $$ 计算第一列（$\\hat{i}$ 的变换）： $$ \\begin{bmatrix} 0 \u0026 2 \\\\ 1 \u0026 0 \\end{bmatrix} \\begin{bmatrix} \\color{green}{1} \\\\ \\color{green}{1} \\end{bmatrix} = 1 \\begin{bmatrix} 0 \\\\ 1 \\end{bmatrix} + 1 \\begin{bmatrix} 2 \\\\ 0 \\end{bmatrix} = \\begin{bmatrix} 2 \\\\ 1 \\end{bmatrix} $$对于基向量j: $$ \\overbrace{\\begin{bmatrix} 0 \u0026 2 \\\\ 1 \u0026 0 \\end{bmatrix}}^{M_2} \\overbrace{\\begin{bmatrix} 1 \u0026 \\color{red}{-2} \\\\ 1 \u0026 \\color{red}{0} \\end{bmatrix}}^{M_1} = \\begin{bmatrix} 2 \u0026 ? \\\\ 1 \u0026 ? \\end{bmatrix} $$ 计算第二列（$\\hat{j}$ 的变换）： $$ \\begin{bmatrix} 0 \u0026 2 \\\\ 1 \u0026 0 \\end{bmatrix} \\begin{bmatrix} \\color{red}{-2} \\\\ \\color{red}{0} \\end{bmatrix} = -2 \\begin{bmatrix} 0 \\\\ 1 \\end{bmatrix} + 0 \\begin{bmatrix} 2 \\\\ 0 \\end{bmatrix} = \\begin{bmatrix} 0 \\\\ -2 \\end{bmatrix} $$最终结果：复合矩阵 $$ \\overbrace{\\begin{bmatrix} a \u0026 b \\\\ c \u0026 d \\end{bmatrix}}^{M_2} \\overbrace{\\begin{bmatrix} e \u0026 {f} \\\\ g \u0026 {h} \\end{bmatrix}}^{M_1} = \\begin{bmatrix} ae + bg \u0026 {af + bh} \\\\ ce + dg \u0026 {cf + dh} \\end{bmatrix} $$ 性质 # 根据矩阵乘法的几何意义可以看到,一个矩阵的变换顺序不同,最后得到的矩阵结果是不同的.\n矩阵乘法： 矩阵乘法性质：\n交换律： $$\\mathbf{M}_1 \\mathbf{M}_2 \\neq \\mathbf{M}_2 \\mathbf{M}_1$$ 先旋转再剪切和先剪切再旋转不同 结合律： $$ \\mathbf{ABC} = \\mathbf{A(BC)} $$ 作用顺序不影响变换结果 分配律： $$ \\mathbf{C(A + B)} = \\mathbf{CA} + \\mathbf{CB} $$ ","date":"2026 年 02 月 14 日","externalUrl":null,"permalink":"/posts/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%AD%A6%E4%B9%A002/","section":"Posts","summary":"","title":"线性代数学习02: 矩阵,线性变换,矩阵乘法,线性变换复合","type":"posts"},{"content":" 课程链接: 【官方双语/合集】线性代数的本质 - 系列合集】\n向量 # 三种看待向量的观点 # 物理:向量是空间中具有方向和长度的箭头 计算机科学:向量是有序的数字列表 数学:向量的相加和相乘有意义 向量法则 # 向量加法 两个方向运动的和先v后w 为什么向量加法遵循平行四边形？ 在几何角度上，两个向量相加在几何上是把两个向量沿着平行于坐标系的方向平移，将两个向量首尾相接，此时终点和“和向量”所指向的终点一致。若把向量看成对特定运动的一种描述，其运动的结果一致。\n向量乘法 长度方向上的缩放 向量乘法的几何意义 对向量按照“标量”(Scalars，在此处可以和number替换理解)\u0026ldquo;来缩放\u0026rdquo;(Scaling). 具体来说是对向量的: 拉伸、缩短、反转。\n线性组合 # 基向量：单位向量i，j是坐标系xy的基向量 线性组合：两个数乘向量的和成为这两个向量的线性组合 平面中任意两个基向量（非共线/非0向量）能组成平面内所有向量 固定两个向量的其中一个，任意变换另一向量就能得到一条直线 张成空间 # 张成空间：给定基向量的所有线性组合的集合，将箭头看作点时，共线时为直线非共线为平面。 一组向量 \\(\\{v_1, v_2, \\dots, v_k\\}\\)的张成空间是这些向量的所有可能线性组合的集合。 即： $$ \\text{Span}\\{v_1, v_2, \\dots, v_k\\} = \\left\\{ c_1 v_1 + c_2 v_2 + \\cdots + c_k v_k \\,\\middle|\\, c_1, c_2, \\dots, c_k \\in \\mathbb{R} \\right\\} $$ 三维空间的张成空间 如果第三个向量恰好落在前两个向量所张成的平面上,所张成的空间不变.\n当三个向量非共面时,张成空间是整个三维空间,线性组合能组合成三维空间的任意向量. 线性相关 # 当有多个向量，移除其中一个而不减小张成的空间时，称它们是线性相关的。\n线性无关 # 如果所有向量都给张成的空间增添了新的维度，称它们是线性无关的\n","date":"2026 年 02 月 05 日","externalUrl":null,"permalink":"/posts/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%AD%A6%E4%B9%A001/","section":"Posts","summary":"","title":"线性代数的本质01:向量,线性组合,张成空间,基向量","type":"posts"},{"content":" 参考的一些笔记\nLecture 04 - Transformation Cont.\n变换的应用 # 旋转 缩放 位置移动 光栅化 二维变换 # 缩放 (Scale Matrix) # 缩放变换是线性代数和计算机图形学中最基础的几何变换之一，用于将图形按比例放大或缩小.\n数学表达 对于平面上任意一点 \\((x, y)\\)，经过缩放因子为 \\(s\\) 的变换后，新坐标 \\((x', y')\\) 为： $$ \\begin{cases} x' = s \\cdot x \\\\ y' = s \\cdot y \\end{cases} $$ 当 \\(s \u003e 1\\) 时，图形放大。\n当 \\(0 \u003c s \u003c 1\\) 时，图形缩小（如图中 \\(s=0.5\\)）。\n当 \\(s = 1\\) 时，图形大小不变（恒等变换）。\n矩阵形式\n$$ \\begin{bmatrix} x' \\\\ y' \\end{bmatrix} = \\begin{bmatrix} s_x \u0026 0 \\\\ 0 \u0026 s_y \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} $$ 翻转 (Reflection Matrix) # 2D 翻转（反射）是把图形沿某条直线（轴）做镜像，得到对称图形。\n常见：沿 x 轴翻转、沿 y 轴翻转、沿原点 $$ \\begin{cases} x' = -x \\\\ y' = y \\end{cases} $$ 矩阵形式 $$ \\begin{bmatrix} x' \\\\ y' \\end{bmatrix} = \\begin{bmatrix} -1 \u0026 0 \\\\ 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} $$ 沿 x 轴：y 变号\n沿 y 轴：x 变号\n沿原点：x、y 都变号\n矩阵都是对角矩阵，对角线上是 ±1\n剪切 (Shear Matrix) # 剪切变换(shear transformation)是空间线性变换之一，是仿射变换的一种原始变换。它指的是类似于四边形不稳定性那种性质，方形变平行四边形，任意一边都可以被拉长的过程。\n$$ \\begin{bmatrix} x' \\\\ y' \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 a \\\\ 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} $$ 旋转 (Rotation Matrix) # $$ R_\\theta = \\begin{bmatrix} \\cos\\theta \u0026 -\\sin\\theta \\\\ \\sin\\theta \u0026 \\cos\\theta \\end{bmatrix} $$ 二维绕原点旋转矩阵推导 几何原理\n取坐标系单位基向量，旋转后坐标直接构成矩阵列：\n基向量 \\(\\boldsymbol{i}=(1,0)\\) 逆时针转\\(\\theta\\) → \\((\\cos\\theta,\\sin\\theta)\\) 基向量 \\(\\boldsymbol{j}=(0,1)\\) 逆时针转\\(\\theta\\) → \\((-\\sin\\theta,\\cos\\theta)\\) 关键记忆点\n第一列：x轴基向量旋转结果 第二列：y轴基向量旋转结果 负号固定在右上 \\(-\\sin\\theta\\) → 仅适用于 逆时针 $$boldsymbol{R}_{-\\theta}=\\begin{bmatrix}\\cos\\theta\u0026-\\sin\\theta\\\\\\sin\\theta\u0026\\cos\\theta\\end{bmatrix}$$ 顺时针旋转θ，负号移到左下： $$ \\boldsymbol{R}_{-\\theta}=\\begin{bmatrix}\\cos\\theta\u0026\\sin\\theta\\\\-\\sin\\theta\u0026\\cos\\theta\\end{bmatrix} $$ 线性变换 = 矩阵(Linear transforms = Matrices) # 核心结论 # 所有二维线性变换，都可以用一个同维度的方阵表示，这是线性代数、计算机图形学的底层核心。\n线性变换的代数形式 # 二维平面内任意点 \\((x,y)\\)经过线性变换后得到新坐标 \\((x',y')\\)，满足一次齐次线性关系： $$ \\begin{cases} x' = ax + by \\\\ y' = cx + dy \\end{cases} $$ 矩阵形式（列向量标准式） # 将线性方程组改写为矩阵 × 列向量的形式，是图形学、线性代数的通用写法： $$ \\begin{bmatrix} x' \\\\ y' \\end{bmatrix} = \\begin{bmatrix} a \u0026 b \\\\ c \u0026 d \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} $$ 向量简化形式 # 用向量符号统一表达，形式更简洁： $$ \\mathbf{x}' = \\mathbf{M}\\mathbf{x} $$ \\(\\mathbf{x}\\)：原坐标列向量 - \\(\\mathbf{x}'\\)：变换后坐标列向量 \\(\\mathbf{M}\\)：线性变换矩阵（2×2方阵） 核心意义 # 一切线性变换（旋转、缩放、翻转、剪切）本质都是一个矩阵 变换的组合 = 矩阵的乘法 变换的逆操作 = 矩阵的逆 仅包含x、y一次项，无常数项、无高次项，才是线性变换 记忆要点 # 线性变换无平移、无常数偏移，仅由坐标的线性组合构成，可完全由方阵描述。\n齐次坐标 (Homogeneous coordinates) # 核心问题：平移变换（Translation）不属于线性变换，无法用普通矩阵乘法表示,引入齐次坐标之后,平移和旋转能统一用矩阵乘法\n平移变换不属于线性变换 统一平移矩阵和线性变换矩阵，使其可以直接参与运算 为向量拓展一个维度，使得一个三维变量可以区分表示为向量和点（w为1为点，w为0为向量，保证向量的平移不变性） 原始仿射变换公式（乘法 + 加法，无法统一）： $$ \\begin{bmatrix}x' \\\\ y'\\end{bmatrix}=\\begin{bmatrix}a \u0026 b \\\\c \u0026 d\\end{bmatrix}\\begin{bmatrix}x \\\\ y\\end{bmatrix}+\\begin{bmatrix}t_x \\\\ t_y\\end{bmatrix} $$解决方案：引入齐次坐标，将平移也转化为矩阵乘法形式\n2D 平移矩阵（齐次坐标形式）： $$ \\begin{bmatrix}x' \\\\ y' \\\\ 1\\end{bmatrix} = \\begin{bmatrix}1 \u0026 0 \u0026 t_x \\\\ 0 \u0026 1 \u0026 t_y \\\\ 0 \u0026 0 \u0026 1\\end{bmatrix}\\begin{bmatrix}x \\\\ y \\\\ 1\\end{bmatrix} = \\begin{bmatrix}x + t_x \\\\ y + t_y \\\\ 1\\end{bmatrix} $$向量平移验证（w=0，不受影响）： $$ \\begin{bmatrix}1 \u0026 0 \u0026 t_x \\\\ 0 \u0026 1 \u0026 t_y \\\\ 0 \u0026 0 \u0026 1\\end{bmatrix}\\begin{bmatrix}x \\\\ y \\\\ 0\\end{bmatrix} = \\begin{bmatrix}x \\\\ y \\\\ 0\\end{bmatrix} $$点与向量的运算规则\n运算 结果 齐次坐标验证 向量 + 向量 向量 (x₁,y₁,0) + (x₂,y₂,0) = (x₁+x₂, y₁+y₂, 0) 点 - 点 向量 (x₁,y₁,1) - (x₂,y₂,1) = (x₁-x₂, y₁-y₂, 0) 点 + 向量 点 (x,y,1) + (dx,dy,0) = (x+dx, y+dy, 1) 点 + 点 中点 (x₁,y₁,1) + (x₂,y₂,1) = (x₁+x₂, y₁+y₂, 2) → 归一化 → (x₁+x₂/2, y₁+y₂/2, 1) 点 + 点 = 中点 的解释 两个点相加得到 (x₁+x₂, y₁+y₂, 2)，w=2 不是标准形式 归一化：除以 w，得到 (x₁+x₂/2, y₁+y₂/2, 1)，这正是两点的中点\n二维齐次坐标 # $$\\text{点: } \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix},\\quad \\begin{bmatrix} x \\\\ y \\\\ w\\end{bmatrix} \\text 表示点 \\begin{bmatrix} \\frac{x}{w} \\\\ \\frac{x}{w} \\\\ 1 \\end{bmatrix}, \\quad \\text{向量: } \\begin{bmatrix} x \\\\ y \\\\ 0 \\end{bmatrix}$$ 二维仿射变换 # 仿射映射 = 线性映射 + 平移变换 $$\\begin{bmatrix}x' \\\\ y'\\end{bmatrix}=\\begin{bmatrix}a \u0026 b \\\\c \u0026 d\\end{bmatrix}\\begin {bmatrix}x \\\\ y\\end{bmatrix}+\\begin {bmatrix}t_x \\\\ t_y\\end{bmatrix}$$ 使用齐次坐标表示仿射变换 $$\\begin{bmatrix} x' \\\\ y' \\\\ 1\\end{bmatrix}=\\begin{bmatrix} a \u0026 b \u0026 t_x \\\\c \u0026 d \u0026 t_y \\\\0 \u0026 0 \u0026 1 \\\\\\end{bmatrix} \\cdot\\begin {bmatrix} x \\\\ y \\\\ 1 \\end {bmatrix}$$ 缩放 (Scale Matrix) $$ \\mathbf{S}(s_x, s_y) = \\begin{pmatrix} s_x \u0026 0 \u0026 0 \\\\ 0 \u0026 s_y \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 翻转 (Reflection Matrix) $$ \\mathbf{R}(\\alpha) = \\begin{pmatrix} \\cos \\alpha \u0026 -\\sin \\alpha \u0026 0 \\\\ \\sin \\alpha \u0026 \\cos \\alpha \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 平移 (Translation Matrix) $$ \\mathbf{T}(t_x, t_y) = \\begin{pmatrix} 1 \u0026 0 \u0026 t_x \\\\ 0 \u0026 1 \u0026 t_y \\\\ 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 逆变换 # M⁻¹ 在矩阵和几何意义上都是变换 M 的逆。\n组合变换 # 平移、旋转、缩放变换可以组合起来\n变换的顺序很重要，不能调换（矩阵的乘法不满足交换律） $$ T_{(1,0)} \\cdot R_{45} \\neq R_{45} \\cdot T_{(1,0)} $$ 可以通过矩阵的乘法来实现组合变换，从右到左的操作 $$ A_n(\\dots A_2(A_1(\\mathbf{x}))) = \\mathbf{A}_n \\cdots \\mathbf{A}_2 \\cdot \\mathbf{A}_1 \\cdot \\begin{pmatrix} x \\\\ y \\\\ 1 \\end{pmatrix} $$ 示例：先旋转 45 度，再平移 (1, 0)\n$$ T_{(1,0)} \\cdot R_{45} \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 0 \u0026 1 \\\\ 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} \\cos 45^\\circ \u0026 -\\sin 45^\\circ \u0026 0 \\\\ \\sin 45^\\circ \u0026 \\cos 45^\\circ \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix} = \\begin{bmatrix} \\cos 45^\\circ \u0026 -\\sin 45^\\circ \u0026 1 \\\\ \\sin 45^\\circ \u0026 \\cos 45^\\circ \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix} $$ 变换顺序：矩阵从右到左依次应用 复合矩阵：通过矩阵乘法，将多个变换合并为一个矩阵，提高计算效率。 齐次坐标：使用 \\(\\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix}\\)​​ 这种三维向量，是为了让平移也能通过线性变换（矩阵乘法）来表示。 旋转中心不在原点：中心移动到原点-\u0026gt;旋转-\u0026gt;中心移动到原位置 $$ \\mathbf{T}(\\mathbf{c}) \\cdot \\mathbf{R}(\\alpha) \\cdot \\mathbf{T}(-\\mathbf{c}) $$ 三维变换 # $$ \\begin{aligned} \u0026 \\text{三维点(3D Point)}: (x, y, z, \\mathbf{1})^T \\\\ \u0026\\text{三维向量(3D Vector)}: (x, y, z, \\mathbf{0})^T \\end{aligned} $$一般来说，(x, y, z, w)（其中 w ≠ 0）表示一个 3D 点： $$ \\text{3D Point} = \\left( \\frac{x}{w}, \\frac{y}{w}, \\frac{z}{w} \\right) $$ 三维仿射变换 # $$ \\underbrace{ \\begin{pmatrix} x' \\\\ y' \\\\ z' \\\\ 1 \\end{pmatrix} }_{\\text{变换后点}} = \\underbrace{ \\begin{pmatrix} a \u0026 b \u0026 c \u0026 t_x \\\\ d \u0026 e \u0026 f \u0026 t_y \\\\ g \u0026 h \u0026 i \u0026 t_z \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} }_{\\text{4×4 仿射变换矩阵}} \\cdot \\underbrace{ \\begin{pmatrix} x \\\\ y \\\\ z \\\\ 1 \\end{pmatrix} }_{\\text{原始点（齐次坐标）}} $$ $$ \\begin{aligned} x' \u0026= a x + b y + c z + t_x \\\\ y' \u0026= d x + e y + f z + t_y \\\\ z' \u0026= g x + h y + i z + t_z \\\\ 1 \u0026= 0 x + 0 y + 0 z + 1 \\end{aligned} $$顺序（同二维）：先线性变换再平移\n缩放(Scale)和平移(Translation) # $$ \\text{Scale: } \\mathbf{S}(s_x, s_y, s_z) = \\begin{pmatrix} s_x \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 s_y \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 s_z \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$$$ \\text{Translation: } \\mathbf{T}(t_x, t_y, t_z) = \\begin{pmatrix} 1 \u0026 0 \u0026 0 \u0026 t_x \\\\ 0 \u0026 1 \u0026 0 \u0026 t_y \\\\ 0 \u0026 0 \u0026 1 \u0026 t_z \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 旋转 (Rotation) # 绕坐标轴旋转 # 旋转矩阵的性质 旋转θ的矩阵： $$ R_\\theta = \\begin{pmatrix} \\cos\\theta \u0026 -\\sin\\theta \\\\ \\sin\\theta \u0026 \\cos\\theta \\end{pmatrix} $$ 旋转-θ的矩阵： $$ R_{-\\theta} = \\begin{pmatrix} \\cos\\theta \u0026 \\sin\\theta \\\\ -\\sin\\theta \u0026 \\cos\\theta \\end{pmatrix} $$ 可以发现: \\(R_{-\\theta}\\)恰好是 \\(R_{\\theta}\\) 的转置，即： $$ R_{-\\theta} = R_\\theta^T $$ 因此: $$ R_\\theta^{-1} = R_\\theta^T $$ 这说明旋转矩阵是正交矩阵，其逆矩阵等于其转置矩阵。\n绕X轴旋转 $$ \\mathbf{R}_x(\\alpha) = \\begin{pmatrix} 1 \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 \\cos\\alpha \u0026 -\\sin\\alpha \u0026 0 \\\\ 0 \u0026 \\sin\\alpha \u0026 \\cos\\alpha \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 绕Y轴旋转\n$$ \\mathbf{R}_y(\\alpha) = \\begin{pmatrix} \\cos\\alpha \u0026 0 \u0026 \\sin\\alpha \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \u0026 0 \\\\ -\\sin\\alpha \u0026 0 \u0026 \\cos\\alpha \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 为什么是 -sina : xoz是顺时针的，顺时针旋转了a,相当于逆时针旋转了-a，旋转是逆时针的，-a的旋转矩阵要求逆，也就是转置\nZ 轴（k）向右（X 正）偏 → 必然带动 X 轴（i）向下（Z 负）偏； 负号仅代表方向，sinα的绝对值是偏移的 “长度”。 绕Z轴旋转 $$ \\mathbf{R}_z(\\alpha) = \\begin{pmatrix} \\cos\\alpha \u0026 -\\sin\\alpha \u0026 0 \u0026 0 \\\\ \\sin\\alpha \u0026 \\cos\\alpha \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{pmatrix} $$ 欧拉角 # 欧拉角描述相对于初始状态的变换，只和最终状态有关，与过程无关。 由于欧拉角（Euler）表示旋转时存在旋转轴的次序，当中间旋转轴旋转使得前后两个轴对齐时，就失去了一个旋转自由度，出现万向节死锁。可以使用四元数（Quaternion）解决该问题。\n$$ \\text{欧拉角:乘法顺序：从右到左执行} \\quad \\\\\\ \\mathbf{R}_{xyz}(\\alpha, \\beta, \\gamma) = \\mathbf{R}_x(\\alpha) \\mathbf{R}_y(\\beta) \\mathbf{R}_z(\\gamma) $$BILIBILI视频 【无伤理解欧拉角中的“万向死锁”现象】 罗德里格斯旋转公式（Rodrigues\u0026rsquo; Rotation Formula） # $$ \\mathbf{R}(\\mathbf{n}, \\alpha) = \\cos(\\alpha) \\mathbf{I} + (1 - \\cos(\\alpha)) \\mathbf{n} \\mathbf{n}^T + \\sin(\\alpha) \\underbrace{ \\begin{pmatrix} 0 \u0026 -n_z \u0026 n_y \\\\ n_z \u0026 0 \u0026 -n_x \\\\ -n_y \u0026 n_x \u0026 0 \\end{pmatrix} }_{\\mathbf{N}} $$其中,反对称矩阵N 为:\n$$ \\mathbf{N} = \\begin{pmatrix} 0 \u0026 -n_z \u0026 n_y \\\\ n_z \u0026 0 \u0026 -n_x \\\\ -n_y \u0026 n_x \u0026 0 \\end{pmatrix} $$推导: # 3D下的旋转矩阵 https://sites.cs.ucsb.edu/~lingqi/teaching/resources/GAMES101_Lecture_04_supp.pdf\n四元数 # 本课程不讲\n","date":"2026 年 02 月 02 日","externalUrl":null,"permalink":"/posts/games101_03/","section":"Posts","summary":"","title":"GAMES101学习笔记03:变换(二维和三维)","type":"posts"},{"content":" 声明：部分内容参考网上博客。\n教程地址: # GAMES101-现代计算机图形学入门-闫令琪\n课程主页: # GAMES101: 现代计算机图形学入门\n作业汇总: http://games-cn.org/forums/topic/allhw/\n一些笔记参考: TA学习笔记\n向量 # 定义 标准化 求和 坐标表示 向量最重要的两个属性：1.方向 2.本身的长度\n向量定义: \\(\\vec{AB} = B - A\\)\n从点 \\(A\\) 指向点 \\(B\\) 的向量 \\(\\vec{AB}\\)，等于终点 \\(B\\) 的坐标减去起点 \\(A\\) 的坐标。\n向量的标准化\n向量的模长 向量的模，就是向量的长度、大小，符号写作：\\(\\|\\vec{a}\\|\\) $$ |\\vec{a}\\| = \\sqrt{a_x^2 + a_y^2} $$用勾股定理可以求出向量的模长\n\\({a_x}\\) : x 轴上的分量（水平）\n\\({a_y}\\) : y 轴上的分量（水平）\n** 向量的模长 = 从原点到该向量终点的直线距离 = 直角三角形的斜边长度（用勾股定理算出来的）**\n单位向量（Unit Vector）：\n长度为 1 的向量，记作 \\(\\hat{a} \\)（用 ^ 标记）。它只保留原向量的方向，丢掉了 “长度” 这个信息。\n$$ \\hat{a} = \\frac{\\vec{a}}{\\|\\vec{a}\\|} $$向量求和\n平行四边形法则 把两个向量的起点放在一起：让向量 a 和 b 从同一个点出发。 以这两个向量为邻边，画一个平行四边形。 从共同起点出发的那条对角线，就是两个向量的和 a+b 三角形法则 把一个向量的起点接到另一个向量的终点上：先画向量 a，再把向量 b 的起点放在 a 的终点上。 从 a 的起点连到 b 的终点，这条线段就是 a+b $$ \\vec{a} = (a_x, a_y),\\quad \\vec{b} = (b_x, b_y) $$$$ \\vec{a} + \\vec{b} = (a_x + b_x, a_y + b_y) $$图形学中的坐标表示\n图形学上一般用列向量表示向量 $$ \\mathbf{A} = \\begin{bmatrix} x \\\\ y \\end{bmatrix}, \\quad \\mathbf{A}^\\text{T} = \\begin{bmatrix} x \u0026 y \\end{bmatrix} $$ 点乘 # 速览 点乘主要应用于求两个单位向量的夹角， 观察两个向量之间是同向、垂直还是反向，可以观察两个向量的接近，若两个向量的点乘接近1则离得很近，若接近0则离得很远 利用投影可将一个向量分解成两个（多个）向量和 $$ \\vec{a} \\cdot \\vec{b} = \\|\\vec{a}\\| \\|\\vec{b}\\| \\cos\\theta $$$$ \\cos\\theta = \\frac{\\vec{a} \\cdot \\vec{b}}{\\|\\vec{a}\\| \\|\\vec{b}\\|} $$ 余弦（cos）是什么 直角三角形里的定义:\n在一个直角三角形中，对于一个锐角 θ： $$ \\cos\\theta = \\frac{\\text{邻边}}{\\text{斜边}} $$邻边：和角 θ 直接相连的那条直角边\n斜边：直角三角形里最长的那条边\n点乘的计算 # 交换律 结合律 分配律 $$ \\cancel{(交换律)} \\newline \\vec{a} \\cdot \\vec{b} = \\vec{b} \\cdot \\vec{a} $$$$ \\cancel{(分配律)} \\newline \\vec{a} \\cdot (\\vec{b} + \\vec{c}) = \\vec{a} \\cdot \\vec{b} + \\vec{a} \\cdot \\vec{c} $$$$ \\cancel{(结合律)} \\newline (k\\vec{a} )\\cdot \\vec{b} = \\vec{a} \\cdot (k\\vec{b}) = k(\\vec{a} \\cdot \\vec{b}) $$ 2D点乘 $$ \\vec{a} \\cdot \\vec{b} = \\begin{pmatrix} x_a \\\\ y_a \\end{pmatrix} \\cdot \\begin{pmatrix} x_b \\\\ y_b \\end{pmatrix} = x_a x_b + y_a y_b $$ 3D点乘 $$ \\vec{a} \\cdot \\vec{b} = \\begin{pmatrix} x_a \\\\ y_a \\\\ z_a \\end{pmatrix} \\cdot \\begin{pmatrix} x_b \\\\ y_b \\\\ z_b \\end{pmatrix} = x_a x_b + y_a y_b + z_a z_b $$ 点乘的作用 # 寻找两个向量之间的夹角 $$ \\cos\\theta = \\frac{\\vec{a} \\cdot \\vec{b}}{\\|\\vec{a}\\| \\|\\vec{b}\\|} $$ 找到一个像两个在另一个向量上的投影 \\(\\vec{b}_\\perp\\) 是 \\(\\vec{b}\\) 在向量 \\(\\vec{a}\\) 上的投影\n投影向量的方向必须与向量A完全一致,表示为\n$$ \\vec{b}_\\perp = k\\hat{a} $$投影长度的计算 $$ k = \\|\\vec{b}_\\perp\\| = \\|\\vec{b}\\|\\cos\\theta $$k 是投影向量 b⊥​ 的模长（大小）。 它等于原向量 b 的模长 ∥b∥ 乘以两向量夹角 θ 的余弦值 cosθ。 这个公式来自直角三角形的边角关系：投影长度就是 b 在 a 方向上的 “影子” 长度 判断向量的方向 cosθ 如何反映方向？ 当 θ\u0026lt;90∘ 时：cosθ\u0026gt;0 → 点积为正。 这说明两个向量的方向大致相同，夹角是锐角。\n当 θ=90∘ 时：cosθ=0 → 点积为 0。 这说明两个向量互相垂直，没有任何方向上的重叠。\n当 θ\u0026gt;90∘ 时：cosθ\u0026lt;0 → 点积为负。 这说明两个向量的方向大致相反，夹角是钝角。\n叉乘 # 速览 AxB=-BxA 判定左右（内外） 若AxB为正则点A在点B在A左侧，若点P在点A、B、C内，则ABXAP，BCxBP,CAxCP结果都为外（内）则P在ABC内 若任意一个结果不同则P在ABC外 定义坐标系 要求：单位向量、互相垂直（点乘为0且叉乘结果为另外一轴） 可以获得任意一个向量分解为多个投影 方向：右手螺旋定则(OpenGL左手坐标系) 作用: 建立三维空间直角坐标系 坐标运算：克莱姆法则 向量叉乘 # 一、标准正交基向量叉乘（3D 坐标系基础） $$ \\begin{aligned} \\vec{x} \\times \\vec{y} \u0026= +\\vec{z} \\\\ \\vec{y} \\times \\vec{x} \u0026= -\\vec{z} \\\\ \\vec{y} \\times \\vec{z} \u0026= +\\vec{x} \\\\ \\vec{z} \\times \\vec{y} \u0026= -\\vec{x} \\\\ \\vec{z} \\times \\vec{x} \u0026= +\\vec{y} \\\\ \\vec{x} \\times \\vec{z} \u0026= -\\vec{y} \\end{aligned} $$ 二、叉乘核心代数性质\n$$ \\begin{aligned} 1.\\ \u0026\\text{反交换性：} \\quad \\vec{a} \\times \\vec{b} = -(\\vec{b} \\times \\vec{a}) \\\\ 2.\\ \u0026\\text{自身叉乘为零向量(长度是0)：} \\quad \\vec{a} \\times \\vec{a} = \\vec{0} \\\\ 3.\\ \u0026\\text{对向量加法的分配律：} \\quad \\vec{a} \\times (\\vec{b} + \\vec{c}) = \\vec{a} \\times \\vec{b} + \\vec{a} \\times \\vec{c} \\\\ 4.\\ \u0026\\text{数乘结合律：} \\quad \\vec{a} \\times (k\\vec{b}) = k(\\vec{a} \\times \\vec{b}) \\quad (k \\in \\mathbb{R}) \\end{aligned} $$ 三、叉乘模长公式（几何意义）\n$$\\begin{aligned}| \\vec{a} \\times \\vec{b}| \u0026= |\\vec{a}| \\cdot |\\vec{b}| \\cdot \\sin\\theta \\quad (0^\\circ \\le \\theta \\le 180^\\circ) \\end{aligned} $$ 作用 # 判定左和右 用叉乘的方向反应右手螺旋定则的旋转的方向反映左右\n向量叉乘判断左右位置 一、场景\n图中向量 a 和 b 都在 x-z 平面上（y 分量为 0）。\n我们想判断 b 相对于 a 的左右位置。\n二、核心计算\n计算叉乘 a×b，结果只有 y 分量：\na×b=(0,ax​bz​−az​bx​,0)\n我们只关心这个 y 分量的正负号。\n三、判断规则\n叉乘 y 分量符号 b 相对于 a 的位置 旋转方向 \u0026gt; 0 在 a 的左侧 逆时针 \u0026lt; 0 在 a 的右侧 顺时针 = 0 与 a 共线 无旋转 结合你的图来看：\nb 在 a 的左侧 所以 a×b 的 y 分量为正 方向指向 +y 轴（向上） 四、一句话总结\n叉乘结果的 y 分量为正 → 左；为负 → 右；为零 → 共线。\n判定内和外 (三角形光栅化基础) 绕同一个方向判断这个向量与其他所有向量的左右位置，如果都一致则在内部。\n三角形内外判断 一、准备工作：定义向量\n我们以图中的三角形 ABC 和内部点 P 为例。\n首先，给三角形的每条边定义一个有向边向量，并定义从边起点到 P 点的向量：\n边 边向量 点向量（从边起点到 P） AB AB=B−A AP=P−A BC BC=C−B BP=P−B CA CA=A−C CP=P−C 二、核心计算：三次叉乘\n对每条边，计算 “边向量” 与 “点向量” 的叉乘。\n我们在 x-y 平面进行计算，叉乘结果只有 z 分量，我们只需关注其正负号。\n第一条边 AB\nc1​=AB×AP\n图中 P 在 AB 的左侧，所以 c1​\u0026gt;0\n第二条边 BC\nc2​=BC×BP\n图中 P 在 BC 的左侧，所以 c2​\u0026gt;0\n第三条边 CA\nc3​=CA×CP\n图中 P 在 CA 的左侧，所以 c3​\u0026gt;0\n三、判断内外：符号一致性\n内部：c1​,c2​,c3​ 全部同号（全部为正，或全部为负）\n就像图中的 P 点，三个叉乘结果都为正，说明它始终在所有边的同一侧。\n外部：如果有任何一个结果的符号与其他不同\n说明点 P 穿过了至少一条边。 四、一句话记忆\n所有叉乘结果同号 → 内部；出现异号 → 外部\n定义直角坐标系 叉乘构建右手坐标系 图中的核心关系是：w=u×v\n这正是叉乘在定义三维空间中的关键作用：\n当 u 和 v 是两个互相垂直的单位向量时，它们的叉乘结果 w 会自动满足：\n垂直于前两者：w⋅u=0 且 w⋅v=0 单位长度：∥w∥=∥u∥∥v∥sin(90∘)=1 这个性质保证了 u,v,w 三者能构成一组右手正交单位基，即一个完美的局部坐标系。\n二、坐标分解的本质\n基于这组基，任何向量 p​ 都可以被分解为：\np​=(p​⋅u)u+(p​⋅v)v+(p​⋅w)w\n这是因为三个基向量互相垂直且长度为 1，点积 (p​⋅u) 就直接代表了 p​ 在 u 方向上的分量大小。\n三、图形学中的应用\n构建局部空间：在渲染中，为每个物体建立一个基于叉乘的局部坐标系，方便计算光照和朝向。 向量投影与分解：将世界空间的向量转换到局部空间，或反之，是 3D 变换的基础。 矩阵 # 速览 （MxN）（NxP）=（MxP） 矩阵没有交换律，只有结合律 转置：(AB)T=BT AT 单位矩阵I（对角阵I）：可以算出矩阵A-1(A逆)，可以用于返回变换前的结果 向量的点乘、叉乘都可以转换为矩阵相乘 点乘：A·B=ATB 叉乘：AxB=A*B 矩阵是由数字（或其他数学对象）按矩形阵列排列而成的集合\n一个 M × N 的矩阵包含 N 行和 N 列，共有 M × N 个元素。\n$$ \\begin{pmatrix} 1 \u0026 2 \\\\ 3 \u0026 4 \\\\ 5 \u0026 6 \\end{pmatrix} $$$$ \\text{向量叉乘写成矩阵乘法形式：} \\vec{a} \\times \\vec{b} = \\begin{pmatrix} 0 \u0026 -z_a \u0026 y_a \\\\ z_a \u0026 0 \u0026 -x_a \\\\ -y_a \u0026 x_a \u0026 0 \\end{pmatrix} \\begin{pmatrix} x_b \\\\ y_b \\\\ z_b \\end{pmatrix} $$ 矩阵乘法 # $$ \\text{若 } A \\text{ 是 } m \\times n \\text{ 矩阵，} B \\text{ 是 } n \\times p \\text{ 矩阵，} \\\\ \\text{则 } C = A \\times B \\text{ 是 } m \\times p \\text{ 矩阵。} $$ 性质 : 结合律，分配律，没有交换律 $$ \\begin{aligned} (A \\cdot B)\\cdot C = A \\cdot (B\\cdot C) \\\\ A \\cdot (B + C) = A \\cdot B + A \\cdot C \\\\ (A + B) \\cdot C = A \\cdot C + B \\cdot C \\end{aligned} $$ 矩阵乘法 · 2×2 简洁演示 设： $$ A=\\begin{pmatrix}1 \u0026 2 \\\\ 3 \u0026 4\\end{pmatrix},\\quad B=\\begin{pmatrix}5 \u0026 6 \\\\ 7 \u0026 8\\end{pmatrix} $$ 能否相乘 $$ A：2\\times2，B：2\\times2 $$ 前列数 = 后行数 → 可相乘\n结果形状 $$ 2\\times2 \\times 2\\times2 = \\boldsymbol{2\\times2}$$ 计算规则（唯一核心） $$ C_{ij} = A_{第i行} \\cdot B_{第j列} \\text{（对应相乘，再相加）}$$ 核心公式: $$ C_{ij} = \\sum_{k=1}^{n} A_{ik} \\cdot B_{kj} $$ 逐元素计算 $$ \\begin{aligned} C_{11} \u0026= 1\\times5 + 2\\times7 = 5+14=19 \\\\ C_{12} \u0026= 1\\times6 + 2\\times8 = 6+16=22 \\\\ C_{21} \u0026= 3\\times5 + 4\\times7 = 15+28=43 \\\\ C_{22} \u0026= 3\\times6 + 4\\times8 = 18+32=50 \\end{aligned} $$ [!VISUAL]- 计算过程可视化 \u0026gt; \u0026gt; $$ \u003e \u003e \\begin{pmatrix} \u003e \u003e \\color{red}{1} \u0026 \\color{red}{2} \\\\ \u003e \u003e 3 \u0026 4 \u003e \u003e \\end{pmatrix} \u003e \u003e \\times \u003e \u003e \\begin{pmatrix} \u003e \u003e \\color{blue}{5} \u0026 6 \\\\ \u003e \u003e \\color{blue}{7} \u0026 8 \u003e \u003e \\end{pmatrix} \u003e \u003e = \u003e \u003e \\begin{pmatrix} \u003e \u003e \\color{purple}{19} \u0026 22 \\\\ \u003e \u003e 43 \u0026 50 \u003e \u003e \\end{pmatrix} \u003e \u003e $$ $$ \\color{red}{1}\\times\\color{blue}{5} + \\color{red}{2}\\times\\color{blue}{7} = \\color{purple}{19} $$ 最终结果 $$ C=A\\times B=\\begin{pmatrix}19 \u0026 22 \\\\ 43 \u0026 50\\end{pmatrix} $$ 重要性质\n矩阵乘法 不满足交换律 $$ A\\times B \\neq B\\times A $$尺寸匹配：前矩阵的列数 = 后矩阵的行数。\n结果尺寸：前行数 × 后列数。\n计算规则：结果的第 i 行第 j 列 = A 的第 i 行 点乘 B 的第 j 列。\n顺序敏感：A×B ≠ B×A，顺序不能乱。\n单位矩阵 # 定义 主对角线全为 1，其余元素全为 0 的方阵，记作 \\(I\\) 或 \\(I_n\\)。\n它就像数字里的 “1”，任何矩阵和它相乘，结果都不变。\n示例 $$ \\begin{aligned} I_2=\\begin{pmatrix}1 \u0026 0 \\\\ 0 \u0026 1\\end{pmatrix},\\quad I_3=\\begin{pmatrix}1 \u0026 0 \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 1\\end{pmatrix} \\end{aligned} $$ 核心性质 . 单位元性质：\\(A \\times I = I \\times A = A\\) 对称矩阵：\\(I^T = I\\) 逆矩阵关系：\\(A \\times A^{-1} = I\\) 矩阵乘积的逆矩阵法则 若 \\(A\\) 和 \\(B\\) 都是可逆矩阵，则它们的乘积 \\(AB\\) 也可逆，且： $$ (AB)^{-1} = B^{-1}A^{-1} $$ 核心逻辑（“穿衣服”比喻） 把矩阵乘法 \\(AB\\) 想象成“先穿袜子 \\(A\\)，再穿鞋 \\(B\\)”。 要撤销这个操作，必须按倒序：“先脱鞋（\\(B^{-1}\\)），再脱袜子（\\(A^{-1}\\)）”。 因此，\\((AB)\\) 的逆就是 \\(B^{-1}A^{-1}\\)。 推导 根据逆矩阵的定义，我们只需验证 \\((AB)(B^{-1}A^{-1}) = I\\)：\n$$ \\begin{aligned} (AB)(B^{-1}A^{-1}) \u0026= A(BB^{-1})A^{-1} \\quad \\text{结合律：只加括号，不换顺序} \\\\ \u0026= AIA^{-1} \\quad \\text{逆矩阵定义：}BB^{-1}=I \\\\ \u0026= AA^{-1} \\quad \\text{单位矩阵性质：}AI=A \\\\ \u0026= I \\quad \\text{逆矩阵定义：}AA^{-1}=I \\end{aligned} $$ 同理可证 \\((B^{-1}A^{-1})(AB) = I\\)，故法则成立。\n转置 # 定义: 将矩阵的行与列互换，沿主对角线翻转。 记为 \\(A^T\\)。 形状规则: 若 \\(A\\) 是 \\(m\\times n\\)，则 \\(A^T\\) 是 \\(n\\times m\\)。 元素规则 \\[ (A^T)_{ij} = A_{ji} \\] 示例（2×2） $$ \\begin{aligned} % 2×2 转置示例 A \u0026= \\begin{pmatrix}1 \u0026 2 \\\\ 3 \u0026 4\\end{pmatrix} \\quad\\Rightarrow\\quad A^T = \\begin{pmatrix}1 \u0026 3 \\\\ 2 \u0026 4\\end{pmatrix} \\\\[1em] % 3×2 转置示例 B \u0026= \\begin{pmatrix}1 \u0026 5 \\\\ 2 \u0026 6 \\\\ 3 \u0026 7\\end{pmatrix} \\quad\\Rightarrow\\quad B^T = \\begin{pmatrix}1 \u0026 2 \u0026 3 \\\\ 5 \u0026 6 \u0026 7\\end{pmatrix} \\end{aligned} $$ 重要性质 $$ \\begin{aligned} (A^T)^T \u0026= A \\\\ (A+B)^T \u0026= A^T+B^T \\\\ (kA)^T \u0026= kA^T \\\\ (AB)^T \u0026= B^TA^T \\quad (\\text{顺序反转}) \\end{aligned} $$ 对称矩阵 满足 \\(A^T=A\\)，元素关于主对角线对称。\n","date":"2026 年 01 月 29 日","externalUrl":null,"permalink":"/posts/games101_02/","section":"Posts","summary":"","title":"GAMES101学习笔记02:向量与线性代数","type":"posts"},{"content":"环境配置使用的是VSCODE + WSL2 进行的配置\n作为自己配置的记录,主要教程来自 # 在 Win10 下配置 GAMES101 开发环境（WSL2）\n启用 Windows 子系统（WSL）功能 # 在 PowerShell（管理员模式）中运行：\ndism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux/all /norestart 解释：\n/all：在所有用户上启用该功能 /norestart：启用功能后不会立即重启 启用虚拟机平台功能 # WSL2 依赖 Windows 虚拟机功能，需要额外启用：\ndism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart 将 WSL 默认版本设置为 WSL2（视情况而定） # wsl --set-default-version 2 Windows 11 默认已安装 WSL2，不需要执行该命令。\nWindows 10 用户 需要执行此命令，否则默认使用 WSL1。\n然后可以去Microsoft Store 下载 Ubuntu20.04\n安装g++和CMake # sudo apt update sudo apt install build-essential cmake 安装 eigen3 和 opencv # sudo apt install libopencv-dev libeigen3-dev 作业7提高题需要使用多线程，如果使用 pthread 库的话，需要修改 CMakeList.txt 文件将 pthread 链接到程序中，在 CMakeList.txt 文件中增加如下文件即可：\nfind_package (Threads) target_link_libraries (RayTracing Threads::Threads) 执行如下命令安装作业8使用的 OpenGL 相关开发库（请参考作业8说明文件）\nsudo apt install libglu1-mesa-dev freeglut3-dev mesa-common-dev xorg-dev 关于作业8的运行说明，请参考 MobaXTerm 的 使用方法小节\n编译测试 # WSL可以直接访问 Windows 的文件系统，但是注意路径有所变化，例如 D盘会被映射到 /mnt/d，所以举例来说，我的 GAMES101 作业0目录在 D:\\Git\\GAMES101\\Assignment0 下，在 WSL 下的路径就变成了 /mnt/d/Git/GAMES101/Assignment0 。\n以作业0为例举例，编译测试方法为：\ncd /mnt/d/Git/GAMES101/Assignment0 mkdir build cd build cmake ../ make 作业0 的CmakeLists.txt\ncmake_minimum_required (VERSION 3.22.1) project (Transformation) find_package(Eigen3 REQUIRED) include_directories(EIGEN3_INCLUDE_DIR) add_executable (Transformation main.cpp) 代码 main.cpp\n#include \u0026lt;cmath\u0026gt; #include \u0026lt;eigen3/Eigen/Core\u0026gt; #include \u0026lt;eigen3/Eigen/Dense\u0026gt; #include \u0026lt;iostream\u0026gt; int main() { // Basic Example of cpp std::cout \u0026lt;\u0026lt; \u0026#34;Example of cpp \\n\u0026#34;; float a = 1.0, b = 2.0; std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; a / b \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; std::sqrt(b) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; std::acos(-1) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; std::sin(30.0 / 180.0 * acos(-1)) \u0026lt;\u0026lt; std::endl; // Example of vector std::cout \u0026lt;\u0026lt; \u0026#34;Example of vector \\n\u0026#34;; // vector definition Eigen::Vector3f v(1.0f, 2.0f, 3.0f); Eigen::Vector3f w(1.0f, 0.0f, 0.0f); // vector output std::cout \u0026lt;\u0026lt; \u0026#34;Example of output \\n\u0026#34;; std::cout \u0026lt;\u0026lt; v \u0026lt;\u0026lt; std::endl; // vector add std::cout \u0026lt;\u0026lt; \u0026#34;Example of add \\n\u0026#34;; std::cout \u0026lt;\u0026lt; v + w \u0026lt;\u0026lt; std::endl; // vector scalar multiply std::cout \u0026lt;\u0026lt; \u0026#34;Example of scalar multiply \\n\u0026#34;; std::cout \u0026lt;\u0026lt; v * 3.0f \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; 2.0f * v \u0026lt;\u0026lt; std::endl; // Example of matrix std::cout \u0026lt;\u0026lt; \u0026#34;Example of matrix \\n\u0026#34;; // matrix definition Eigen::Matrix3f i, j; i \u0026lt;\u0026lt; 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0; j \u0026lt;\u0026lt; 2.0, 3.0, 1.0, 4.0, 6.0, 5.0, 9.0, 7.0, 8.0; // matrix output std::cout \u0026lt;\u0026lt; \u0026#34;Example of output \\n\u0026#34;; std::cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; std::endl; // matrix add i + j // matrix scalar multiply i * 2.0 // matrix multiply i * j // matrix multiply vector i * v return 0; } 输出\npan@DESKTOP-IO8PALN:~/games101study/build$ /home/pan/games101study/build/Transformation Example of cpp 1 0.5 1.41421 3.14159 0.5 Example of vector Example of output 1 2 3 Example of add 2 2 3 Example of scalar multiply 3 6 9 2 4 6 Example of matrix Example of output 1 2 3 4 5 6 7 8 9 ","date":"2026 年 01 月 29 日","externalUrl":null,"permalink":"/posts/games101_01/","section":"Posts","summary":"","title":"GAMES101学习笔记01: 环境配置","type":"posts"},{"content":"","date":"2023 年 03 月 27 日","externalUrl":null,"permalink":"/tags/golang/","section":"Tags","summary":"","title":"Golang","type":"tags"},{"content":"教程:Easy 搞定 Golang 设计模式 (yuque.com)\n面向对象设计原则 # 原则的目的： 高内聚，低耦合\n单一职责原则 # 类的职责单一，对外只提供一种功能，而引起类变化的原因都应该只有一个。\npackage main import \u0026#34;fmt\u0026#34; type ClothesShop struct {} func (cs *ClothesShop) OnShop() { fmt.Println(\u0026#34;休闲的装扮\u0026#34;) } type ClothesWork struct {} func (cw *ClothesWork) OnWork() { fmt.Println(\u0026#34;工作的装扮\u0026#34;) } func main() { //工作的时候 cw := new(ClothesWork) cw.OnWork() //shopping的时候 cs := new(ClothesShop) cs.OnShop() } 开闭原则 # 开闭原则 (Open-Closed Principle, OCP)**: 类的改动是通过增加代码进行的，而不是修改源代码。\n平铺式设计 # package main import \u0026#34;fmt\u0026#34; //我们要写一个类,Banker银行业务员 type Banker struct { } //存款业务 func (this *Banker) Save() { fmt.Println( \u0026#34;进行了 存款业务...\u0026#34;) } //转账业务 func (this *Banker) Transfer() { fmt.Println( \u0026#34;进行了 转账业务...\u0026#34;) } //支付业务 func (this *Banker) Pay() { fmt.Println( \u0026#34;进行了 支付业务...\u0026#34;) } func main() { banker := \u0026amp;Banker{} banker.Save() banker.Transfer() banker.Pay() } 这样设计我们看到功能是实现了,但是带来的维护问题也非常大.增加一个新的功能可能对原来稳定的代码造成影响,不利于后期维护.\n开闭原则设计 # 开闭原则: 一个软件实体如类、模块和函数应该对扩展开放，对修改关闭。\n简单的说就是在修改需求的时候，应该尽量通过扩展来实现变化，而不是通过修改已有代码来实现变化\n这里使用三个业务员实例分别实现接口,这样即使存款业务员的业务出问题了,不会影响到转账和支付.\npackage main import \u0026#34;fmt\u0026#34; //抽象的银行业务员 type AbstractBanker interface{ DoBusi()\t//抽象的处理业务接口 } //存款的业务员 type SaveBanker struct { //AbstractBanker } func (sb *SaveBanker) DoBusi() { fmt.Println(\u0026#34;进行了存款\u0026#34;) } //转账的业务员 type TransferBanker struct { //AbstractBanker } func (tb *TransferBanker) DoBusi() { fmt.Println(\u0026#34;进行了转账\u0026#34;) } //支付的业务员 type PayBanker struct { //AbstractBanker } func (pb *PayBanker) DoBusi() { fmt.Println(\u0026#34;进行了支付\u0026#34;) } func main() { //进行存款 sb := \u0026amp;SaveBanker{} sb.DoBusi() //进行转账 tb := \u0026amp;TransferBanker{} tb.DoBusi() //进行支付 pb := \u0026amp;PayBanker{} pb.DoBusi() } 接口的意义 # 实际上接口的最大的意义就是实现多态的思想，就是我们可以根据 interface 类型来设计 API 接口，那么这种 API 接口的适应能力不仅能适应当下所实现的全部模块，也适应未来实现的模块来进行调用。 调用未来 可能就是接口的最大意义所在吧，这也是为什么架构师那么值钱，因为良好的架构师是可以针对 interface 设计一套框架，在未来许多年却依然适用。\n依赖倒转原则 # 依赖倒转原则：模块与模块依赖抽象而不是具体实现\n耦合度极高的模块关系设计 # package main import \u0026#34;fmt\u0026#34; // === \u0026gt; 奔驰汽车 \u0026lt;=== type Benz struct { } func (this *Benz) Run() { fmt.Println(\u0026#34;Benz is running...\u0026#34;) } // === \u0026gt; 宝马汽车 \u0026lt;=== type BMW struct { } func (this *BMW) Run() { fmt.Println(\u0026#34;BMW is running ...\u0026#34;) } //===\u0026gt; 司机张三 \u0026lt;=== type Zhang3 struct { //... } func (zhang3 *Zhang3) DriveBenZ(benz *Benz) { fmt.Println(\u0026#34;zhang3 Drive Benz\u0026#34;) benz.Run() } func (zhang3 *Zhang3) DriveBMW(bmw *BMW) { fmt.Println(\u0026#34;zhang3 drive BMW\u0026#34;) bmw.Run() } //===\u0026gt; 司机李四 \u0026lt;=== type Li4 struct { //... } func (li4 *Li4) DriveBenZ(benz *Benz) { fmt.Println(\u0026#34;li4 Drive Benz\u0026#34;) benz.Run() } func (li4 *Li4) DriveBMW(bmw *BMW) { fmt.Println(\u0026#34;li4 drive BMW\u0026#34;) bmw.Run() } func main() { //业务1 张3开奔驰 benz := \u0026amp;Benz{} zhang3 := \u0026amp;Zhang3{} zhang3.DriveBenZ(benz) //业务2 李四开宝马 bmw := \u0026amp;BMW{} li4 := \u0026amp;Li4{} li4.DriveBMW(bmw) } 这段代码没有使用 interface ,可以看到如果要来个 新司机,开一辆新车又要重写一遍 run ,Drive,这样大大增加了代码量,降低了后续维护的便利性.\n面向抽象层依赖倒转 # 如上图所示，如果我们在设计一个系统的时候，将模块分为 3 个层次，抽象层、实现层、业务逻辑层。那么，我们首先将抽象层的模块和接口定义出来，这里就需要了interface接口的设计，然后我们依照抽象层，依次实现每个实现层的模块，在我们写实现层代码的时候，实际上我们只需要参考对应的抽象层实现就好了，实现每个模块，也和其他的实现的模块没有关系，这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口，而和其他模块没关系，依赖关系单一。系统容易扩展和维护。\n我们在指定业务逻辑也是一样，只需要参考抽象层的接口来业务就好了，抽象层暴露出来的接口就是我们业务层可以使用的方法，然后可以通过多态的线下，接口指针指向哪个实现模块，调用了就是具体的实现方法，这样我们业务逻辑层也是依赖抽象成编程。\n我们就将这种的设计原则叫做依赖倒转原则。\npackage main import \u0026#34;fmt\u0026#34; // ===== \u0026gt; 抽象层 \u0026lt; ======== type Car interface { Run() } type Driver interface { Drive(car Car) } // ===== \u0026gt; 实现层 \u0026lt; ======== type BenZ struct { //... } func (benz * BenZ) Run() { fmt.Println(\u0026#34;Benz is running...\u0026#34;) } type Bmw struct { //... } func (bmw * Bmw) Run() { fmt.Println(\u0026#34;Bmw is running...\u0026#34;) } type Zhang_3 struct { //... } func (zhang3 *Zhang_3) Drive(car Car) { fmt.Println(\u0026#34;Zhang3 drive car\u0026#34;) car.Run() } type Li_4 struct { //... } func (li4 *Li_4) Drive(car Car) { fmt.Println(\u0026#34;li4 drive car\u0026#34;) car.Run() } // ===== \u0026gt; 业务逻辑层 \u0026lt; ======== func main() { //张3 开 宝马 var bmw Car bmw = \u0026amp;Bmw{} var zhang3 Driver zhang3 = \u0026amp;Zhang_3{} zhang3.Drive(bmw) //李4 开 奔驰 var benz Car benz = \u0026amp;BenZ{} var li4 Driver li4 = \u0026amp;Li_4{} li4.Drive(benz) } 合成复用原则 # 合成复用原则：通过组合来实现父类方法\n如果使用继承，会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合，就降低了这种依赖关系。对于继承和组合，优先使用组合。\n这里看到如果创建 worker 对象还需要初始化父类 Person ,而创建 teacher 对象则不需要.\npackage main import \u0026#34;fmt\u0026#34; // Person 父类 type Person struct { Name string Age int } func (p *Person) GetName() { fmt.Println(\u0026#34;Name: \u0026#34;, p.Name) } func (p *Person) New(name string, age int) { p.Name = name p.Age = age } // Worker 继承父类 type Worker struct { Person } func (w *Worker) GetName() { fmt.Println(\u0026#34;Name: \u0026#34;, w.Name) } func (w *Worker) SetName(name string) { w.Name = name } // Teacher 组合方式 type Teacher struct { P *Person Name string } func (t *Teacher) GetName() { fmt.Println(\u0026#34;Name: \u0026#34;, t.Name) } func (t *Teacher) SetName(name string) { t.Name = name } func main() { // 继承 w := Worker{Person{ Name: \u0026#34;jack\u0026#34;, Age: 10, }} w.GetName() // 组合 t := Teacher{ P: nil, Name: \u0026#34;mike\u0026#34;, } t.GetName() } 输出结果:\nName: jack Name: mike 迪米特法则 # 迪米特法则：依赖第三方来实现解耦\n设计模式六大原则(五)\u0026mdash;-迪米特法则 - 盛开的太阳 - 博客园 (cnblogs.com)\n创造型模式 # 简单工厂模式 # 不使用工厂模式\n依赖关系: 业务逻辑层 ---\u0026gt; 基础类模块\npackage main import \u0026#34;fmt\u0026#34; //水果类 type Fruit struct { //... //... //... } func (f *Fruit) Show(name string) { if name == \u0026#34;apple\u0026#34; { fmt.Println(\u0026#34;我是苹果\u0026#34;) } else if name == \u0026#34;banana\u0026#34; { fmt.Println(\u0026#34;我是香蕉\u0026#34;) } else if name == \u0026#34;pear\u0026#34; { fmt.Println(\u0026#34;我是梨\u0026#34;) } } //创建一个Fruit对象 func NewFruit(name string) *Fruit { fruit := new(Fruit) if name == \u0026#34;apple\u0026#34; { //创建apple逻辑 } else if name == \u0026#34;banana\u0026#34; { //创建banana逻辑 } else if name == \u0026#34;pear\u0026#34; { //创建pear逻辑 } return fruit } func main() { apple := NewFruit(\u0026#34;apple\u0026#34;) apple.Show(\u0026#34;apple\u0026#34;) banana := NewFruit(\u0026#34;banana\u0026#34;) banana.Show(\u0026#34;banana\u0026#34;) pear := NewFruit(\u0026#34;pear\u0026#34;) pear.Show(\u0026#34;pear\u0026#34;) } 不适用工厂模式带来的问题:\n(1) 在 Fruit 类中包含很多if…else…代码块，整个类的代码相当冗长，代码越长，阅读难度、维护难度和测试难度也越大；而且大量条件语句的存在还将影响系统的性能，程序在执行过程中需要做大量的条件判断。\n(2) Fruit 类的职责过重，它负责初始化和显示所有的水果对象，将各种水果对象的初始化代码和显示代码集中在一个类中实现，违反了“单一职责原则”，不利于类的重用和维护；\n(3) 当需要增加新类型的水果时，必须修改 Fruit 类的构造函数 NewFruit() 和其他相关方法源代码，违反了“开闭原则”。\n这样如果要增加新的Fruit种类就需要更改 Fruit类中的代码,耦合度高,不利于后期维护.这时候就需要个中间层(工厂模块层)将逻辑层也业务层隔离,实现解耦.\n业务逻辑层 ---\u0026gt; 工厂模块 ---\u0026gt; 基础类模块\n抽象层, 定义一个水果接口,具体类实现该接口.\n// ======= 抽象层 ========= //水果类(抽象接口) type Fruit interface { Show()\t//接口的某方法 } 实体类层,实现接口方法\n// ====== 实体类层 ======= type Apple struct { Fruit } func (apple *Apple) Show() { fmt.Println(\u0026#34;我是苹果\u0026#34;) } type Banana struct { Fruit } func (banana *Banana) Show() { fmt.Println(\u0026#34;我是香蕉\u0026#34;) } type Orange struct { Fruit } func (orange *Orange) Show() { fmt.Println(\u0026#34;我是橘子\u0026#34;) } 工厂模块层 ,生产水果类,返回水果实体类指针.\n// ====== 工厂模块 ====== type Factory struct {} func (fac *Factory) CreateFruit(kind string) Fruit { var fruit Fruit if kind == \u0026#34;apple\u0026#34; { fruit = new(Apple) } else if kind == \u0026#34;banana\u0026#34; { fruit = new(Banana) } else if kind == \u0026#34;pear\u0026#34; { fruit = new(Pear) } return fruit } 业务逻辑层,使用工厂模块生产水果实体类.\n// ====== 业务逻辑层 ====== func main() { factory := new(Factory) apple := factory.CreateFruit(\u0026#34;apple\u0026#34;) apple.Show() banana := factory.CreateFruit(\u0026#34;banana\u0026#34;) banana.Show() pear := factory.CreateFruit(\u0026#34;pear\u0026#34;) pear.Show() } 完整代码\n/* 简单工厂模式 */ // ======= 抽象层 ========= //水果类(抽象接口) type Fruit interface { Show()\t//接口的某方法 } // ====== 实体类层 ======= type Apple struct { Fruit } func (apple *Apple) Show() { fmt.Println(\u0026#34;我是苹果\u0026#34;) } type Banana struct { Fruit } func (banana *Banana) Show() { fmt.Println(\u0026#34;我是香蕉\u0026#34;) } type Orange struct { Fruit } func (orange *Orange) Show() { fmt.Println(\u0026#34;我是橘子\u0026#34;) } // ====== 工厂模块 ====== type Factory struct {} func (fac *Factory) CreateFruit(kind string) Fruit { var fruit Fruit if kind == \u0026#34;apple\u0026#34; { fruit = new(Apple) } else if kind == \u0026#34;banana\u0026#34; { fruit = new(Banana) } else if kind == \u0026#34;pear\u0026#34; { fruit = new(Pear) } return fruit } // ====== 业务逻辑层 ====== func main() { factory := new(Factory) apple := factory.CreateFruit(\u0026#34;apple\u0026#34;) apple.Show() banana := factory.CreateFruit(\u0026#34;banana\u0026#34;) banana.Show() pear := factory.CreateFruit(\u0026#34;pear\u0026#34;) pear.Show() } 简单工厂模式的优缺点:\n优点：\n实现了对象创建和使用的分离。 不需要记住具体类名，记住参数即可，减少使用者记忆量。 缺点：\n对工厂类职责过重，一旦不能工作，系统受到影响。 增加系统中类的个数，复杂度和理解度增加。 违反“开闭原则”，添加新产品需要修改工厂逻辑，工厂越来越复杂。 适用场景：\n工厂类负责创建的对象比较少，由于创建的对象较少，不会造成工厂方法中的业务逻辑太过复杂。 客户端只知道传入工厂类的参数，对于如何创建对象并不关心。 工厂方法模式 # 工厂方法模式中的角色和职责: 抽象工厂（Abstract Factory）角色：工厂方法模式的核心，任何工厂类都必须实现这个接口。\n工厂（Concrete Factory）角色：具体工厂类是抽象工厂的一个实现，负责实例化产品对象。\n抽象产品（Abstract Product）角色：工厂方法模式所创建的所有对象的父类，它负责描述所有实例所共有的公共接口。\n具体产品（Concrete Product）角色：工厂方法模式所创建的具体实例对象\n简单工厂模式 + “开闭原则” = 工厂方法模式\n为了避免简单工厂因为工厂类职责过重带来的问题,就由抽象层抽象出来水果类和工厂类来实现一个水果一个对应的工厂. 抽象层\n// ======= 抽象层 ========= // 水果接口 type Fruit interface { Show() //接口的某方法 } // 工厂接口 type AbstractFactory interface { CreateFruit() Fruit // 生成水果的抽象方法 } 实体类层\n// ===== 实体类层 ===== // 苹果实体类 type Apple struct { Fruit } func (apple *Apple) Show() { fmt.Println(\u0026#34;Apple\u0026#34;) } // 香蕉实体类 type Banana struct { Fruit } func (banana *Banana) Show() { fmt.Println(\u0026#34;Banana\u0026#34;) } 工厂模块层\n// ==== 工厂模块层 ===== // 苹果类对应的苹果工厂 type AppleFactory struct { AbstractFactory } func (af *AppleFactory) CreateFruit() Fruit { var fruit Fruit fruit = new(Apple) return fruit } // 香蕉对应的工厂 type BananaFactory struct { AbstractFactory } func (bf *BananaFactory) CreateFruit() Fruit { var fruit Fruit fruit = new(Banana) return fruit } 业务逻辑层\n// ===== 业务实现层 ===== func main() { // 创建苹果工厂生成苹果实例 var appleFactory AppleFactory apple := appleFactory.CreateFruit() apple.Show() // 创建香蕉工厂生成香蕉实例 var bananaFactory BananaFactory banana := bananaFactory.CreateFruit() banana.Show() } 完整代码\n/* 工厂方法模式 */ package main import \u0026#34;fmt\u0026#34; // ======= 抽象层 ========= // 水果接口 type Fruit interface { Show() //接口的某方法 } // 工厂接口 type AbstractFactory interface { CreateFruit() Fruit // 生成水果的抽象方法 } // ===== 实体类层 ===== // type Apple struct { Fruit } func (apple *Apple) Show () { fmt.Println(\u0026#34;Apple\u0026#34;) } type Banana struct { Fruit } func (banana *Banana) Show() { fmt.Println(\u0026#34;Banana\u0026#34;) } // ==== 工厂模块层 ===== type AppleFactory struct{ AbstractFactory } func (af *AppleFactory) CreateFruit() Fruit{ var fruit Fruit fruit = new(Apple) return fruit } type BananaFactory struct{ AbstractFactory } func (bf *BananaFactory) CreateFruit() Fruit{ var fruit Fruit fruit = new(Banana) return fruit } // ===== 业务实现层 ===== func main() { // 创建苹果工厂生成苹果实例 var appleFactory AppleFactory apple := appleFactory.CreateFruit() apple.Show() // 创建香蕉工厂生成香蕉实例 var bananaFactory BananaFactory banana := bananaFactory.CreateFruit() banana.Show() } 工厂方法模式的优缺点:\n优点：\n不需要记住具体类名，甚至连具体参数都不用记忆。 实现了对象创建和使用的分离。 系统的可扩展性也就变得非常好，无需修改接口和原类。 对于新产品的创建，符合开闭原则。 缺点：\n增加系统中类的个数，复杂度和理解度增加。 增加了系统的抽象性和理解难度。 适用场景：\n客户端不知道它所需要的对象的类。 抽象工厂类通过其子类来指定创建哪个对象 抽象工厂方法模式 # 工厂方法模式通过引入工厂等级结构，解决了简单工厂模式中工厂类职责太重的问题，但由于工厂方法模式中的每个工厂只生产一类产品，可能会导致系统中存在大量的工厂类，势必会增加系统的开销。\n因此，可以考虑将一些相关的产品组成一个“产品族”，由同一个工厂来统一生产.这就是抽象工厂方法模式.\n抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合。 从设计层面看，抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)。 将工厂抽象成两层，抽象工厂 和 具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂集合， 更利于代码的维护和扩展。 ———————————————— 版权声明：本文为CSDN博主「Mitsuha三葉」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/qq_42804736/article/details/115168313\n产品族和产品等级结构 # 上图表示“产品族”和“产品登记结构”的关系。 产品族：具有同一个地区、同一个厂商、同一个开发包、同一个组织模块等，但是具备不同特点或功能的产品集合，称之为是一个产品族。\n产品等级结构：具有相同特点或功能，但是来自不同的地区、不同的厂商、不同的开发包、不同的组织模块等的产品集合，称之为是一个产品等级结构。\n当程序中的对象可以被划分为产品族和产品等级结构之后，那么“抽象工厂方法模式”才可以被适用。\n“抽象工厂方法模式”是针对“产品族”进行生产产品.\n抽象工厂方法模式实现 # 抽象层\n/* 抽象工厂方法 */ // ======= 抽象层 ========= // AbstractApple 接口 type AbstractApple interface { ShowApple() } // AbstractBanana 接口 type AbstractBanana interface { ShowBanana() } // Factory 接口 type Factory interface { CreateApple() AbstractApple CreateBanana() AbstractBanana } 实现层\n// ===== 实现类层 ===== // 中国产品族 type ChinaApple struct { AbstractApple } // 实现 AbstractApple 接口 func (ca *ChinaApple) ShowApple() { fmt.Println(\u0026#34;ChinaApple\u0026#34;) } type ChinaBanana struct { AbstractBanana } // 实现 AbstractBanana 接口 func (cb *ChinaBanana) ShowBanana() { fmt.Println(\u0026#34;ChinaBanana\u0026#34;) } type ChinaFactory struct { Factory } // 实现 Factory 接口 func (cf *ChinaFactory) CreateApple() AbstractApple { var chinaApple ChinaApple return \u0026amp;chinaApple } // 实现 Factory 接口 func (cf *ChinaFactory) CreateBanana() AbstractBanana { var chinabanana ChinaBanana return \u0026amp;chinabanana } // 日本产品族 type JapanApple struct { AbstractApple } func (ja *JapanApple) ShowApple() { fmt.Println(\u0026#34;JapanApple\u0026#34;) } type JapanBanana struct { AbstractBanana } func (jb *JapanBanana) ShowBanana() { fmt.Println(\u0026#34;JapanBanana\u0026#34;) } type JapanFactory struct { Factory } func (jf *JapanFactory) CreateApple() AbstractApple { var japanApple JapanApple return \u0026amp;japanApple } func (jf *JapanFactory) CreateBanana() AbstractBanana { var japanBanana JapanBanana return \u0026amp;japanBanana } 业务层\nfunc main() { // 中国工厂 var cf ChinaFactory ca := cf.CreateApple() cb := cf.CreateBanana() ca.ShowApple() cb.ShowBanana() // 日本工厂 var jf JapanFactory ja := jf.CreateApple() jb := jf.CreateBanana() ja.ShowApple() jb.ShowBanana() } 完整代码\npackage main import \u0026#34;fmt\u0026#34; // ======= 抽象层 ========= // AbstractApple 接口 type AbstractApple interface { ShowApple() } // AbstractBanana 接口 type AbstractBanana interface { ShowBanana() } // Factory 接口 type Factory interface { CreateApple() AbstractApple CreateBanana() AbstractBanana } // ===== 实现类层 ===== // 中国产品族 type ChinaApple struct { AbstractApple } // 实现 AbstractApple 接口 func (ca *ChinaApple) ShowApple() { fmt.Println(\u0026#34;ChinaApple\u0026#34;) } type ChinaBanana struct { AbstractBanana } // 实现 AbstractBanana 接口 func (cb *ChinaBanana) ShowBanana() { fmt.Println(\u0026#34;ChinaBanana\u0026#34;) } type ChinaFactory struct { Factory } // 实现 Factory 接口 func (cf *ChinaFactory) CreateApple() AbstractApple { var chinaApple ChinaApple return \u0026amp;chinaApple } // 实现 Factory 接口 func (cf *ChinaFactory) CreateBanana() AbstractBanana { var chinabanana ChinaBanana return \u0026amp;chinabanana } // 日本产品族 type JapanApple struct { AbstractApple } func (ja *JapanApple) ShowApple() { fmt.Println(\u0026#34;JapanApple\u0026#34;) } type JapanBanana struct { AbstractBanana } func (jb *JapanBanana) ShowBanana() { fmt.Println(\u0026#34;JapanBanana\u0026#34;) } type JapanFactory struct { Factory } func (jf *JapanFactory) CreateApple() AbstractApple { var japanApple JapanApple return \u0026amp;japanApple } func (jf *JapanFactory) CreateBanana() AbstractBanana { var japanBanana JapanBanana return \u0026amp;japanBanana } func main() { // 中国工厂 var cf ChinaFactory ca := cf.CreateApple() cb := cf.CreateBanana() ca.ShowApple() cb.ShowBanana() // 日本工厂 var jf JapanFactory ja := jf.CreateApple() jb := jf.CreateBanana() ja.ShowApple() jb.ShowBanana() } 输出结果:\nChinaApple ChinaBanana JapanApple JapanBanana 优点：\n拥有工厂方法模式的优点 当一个产品族中的多个对象被设计成一起工作时，它能够保证客户端始终只使用同一个产品族中的对象。\n3 增加新的产品族很方便，无须修改已有系统，符合“开闭原则”。 缺点：\n增加新的产品等级结构麻烦，需要对原有系统进行较大的修改，甚至需要修改抽象层代码，这显然会带来较大的不便，违背了“开闭原则”。 教程里面的练习:\n练习： 设计一个电脑主板架构，电脑包括（显卡，内存，CPU）3个固定的插口，显卡具有显示功能（display，功能实现只要打印出意义即可），内存具有存储功能（storage），cpu具有计算功能（calculate）。 现有Intel厂商，nvidia厂商，Kingston厂商，均会生产以上三种硬件。 要求组装两台电脑， 1台（Intel的CPU，Intel的显卡，Intel的内存） 1台（Intel的CPU， nvidia的显卡，Kingston的内存） 用抽象工厂模式实现。\n//我的代码 package main import ( \u0026#34;fmt\u0026#34; ) /******* 抽象层 ********/ type AbstractCPU interface { calculate() } type AbstractVedioCard interface { display() } type AbstractRAM interface { storage() } type Factory interface { CreateCPU() AbstractCPU CreateRAM() AbstractRAM CreateVedioCard() AbstractVedioCard } type AbstractComputer interface { SetCPU(AbstractCPU) SetRAM(AbstractRAM) SetVedioCard(AbstractVedioCard) } /******* 实体类层 ********/ // INTEL type IntelFactory struct { Factory } func (ifac *IntelFactory) CreateCPU() AbstractCPU { var ic IntelCPU return \u0026amp;ic } func (ifac *IntelFactory) CreateRAM() AbstractRAM { var ir IntelRAM return \u0026amp;ir } func (ifac *IntelFactory) CreateVedioCard() AbstractVedioCard { var ivc IntelVedioCard return \u0026amp;ivc } type IntelCPU struct { AbstractCPU } func (ic *IntelCPU) calculate() { fmt.Println(\u0026#34;IntelCPU_Calculate\u0026#34;) } type IntelVedioCard struct { AbstractVedioCard } func (ivc *IntelVedioCard) display() { fmt.Println(\u0026#34;IntelVedioCard_Display\u0026#34;) } type IntelRAM struct { AbstractRAM } func (ir *IntelRAM) storage() { fmt.Println(\u0026#34;IntelRAM_Storage\u0026#34;) } // NVIDIA族 Factory type NvidiaFactory struct { Factory } func (nfac *NvidiaFactory) CreateCPU() AbstractCPU { var nc NvidiaCPU return \u0026amp;nc } func (nfac *NvidiaFactory) CreateRAM() AbstractRAM { var nr NvidiaRAM return \u0026amp;nr } func (nfac *NvidiaFactory) CreateVedioCard() AbstractVedioCard { var nvc NvidiaVedioCard return \u0026amp;nvc } type NvidiaCPU struct { AbstractCPU } func (nc *NvidiaCPU) calculate() { fmt.Println(\u0026#34;NvidiaCPU_Calculate\u0026#34;) } type NvidiaVedioCard struct { AbstractVedioCard } func (nvc *NvidiaVedioCard) display() { fmt.Println(\u0026#34;NvidiaVedioCard_Display\u0026#34;) } type NvidiaRAM struct { AbstractRAM } func (nr *NvidiaRAM) storage() { fmt.Println(\u0026#34;NvidiaRAM_Storage\u0026#34;) } // NVIDIA族 Factory type KingstonFactory struct { Factory } func (nfac *KingstonFactory) CreateCPU() AbstractCPU { var kc KingstonCPU return \u0026amp;kc } func (nfac *KingstonFactory) CreateRAM() AbstractRAM { var kr KingstonRAM return \u0026amp;kr } func (nfac *KingstonFactory) CreateVedioCard() AbstractVedioCard { var kvc KingstonVedioCard return \u0026amp;kvc } type KingstonCPU struct { AbstractCPU } func (kc *KingstonCPU) calculate() { fmt.Println(\u0026#34;KingstonCPU_Calculate\u0026#34;) } type KingstonVedioCard struct { AbstractVedioCard } func (kvc *KingstonVedioCard) display() { fmt.Println(\u0026#34;KingstonVedioCard_Display\u0026#34;) } type KingstonRAM struct { AbstractRAM } func (kr *KingstonRAM) storage() { fmt.Println(\u0026#34;KingstonRAM_Storage\u0026#34;) } // Computer type Computer struct { CPU AbstractCPU RAM AbstractRAM VedioCard AbstractVedioCard } func (c *Computer) SetCPU(CPU AbstractCPU) { c.CPU = CPU } func (c *Computer) SetRAM(RAM AbstractRAM) { c.RAM = RAM } func (c *Computer) SetVedioCard(VedioCard AbstractVedioCard) { c.VedioCard = VedioCard } /******* 业务实现层 ********/ func main() { // 工厂 var ifac IntelFactory var nfac NvidiaFactory var kfac KingstonFactory ic := ifac.CreateCPU() ir := ifac.CreateRAM() ivc := ifac.CreateVedioCard() nvc := nfac.CreateVedioCard() kr := kfac.CreateRAM() var c1 Computer c1.SetCPU(ic) c1.SetRAM(ir) c1.SetVedioCard(ivc) c1.CPU.calculate() c1.RAM.storage() c1.VedioCard.display() var c2 Computer c2.SetCPU(ic) c2.SetRAM(kr) c2.SetVedioCard(nvc) c2.CPU.calculate() c2.RAM.storage() c2.VedioCard.display() } 运行结果:\nIntelCPU_Calculate IntelRAM_Storage IntelVedioCard_Display IntelCPU_Calculate KingstonRAM_Storage NvidiaVedioCard_Display 单例模式 # Singleton（单例）：在单例类的内部实现只生成一个实例，同时它提供一个静态的getInstance()工厂方法，让客户可以访问它的唯一实例；为了防止在外部对其实例化，将其构造函数设计为私有；在单例类内部定义了一个Singleton类型的静态对象，作为外部共享的唯一实例。\n单例模式要解决的问题是：\n保证一个类永远只能有一个对象，且该对象的功能依然能被其他模块使用。\n单例模式(饿汉模式)\npackage main import \u0026#34;fmt\u0026#34; /* 三个要点： 一是某个类只能有一个实例； 二是它必须自行创建这个实例； 三是它必须自行向整个系统提供这个实例。 */ /* 保证一个类永远只能有一个对象 */ //1、保证这个类非公有化，外界不能通过这个类直接创建一个对象 // 那么这个类就应该变得非公有访问 类名称首字母要小写 type singelton struct {} //2、但是还要有一个指针可以指向这个唯一对象，但是这个指针永远不能改变方向 // Golang中没有常指针概念，所以只能通过将这个指针私有化不让外部模块访问 var instance *singelton = new(singelton) //3、如果全部为私有化，那么外部模块将永远无法访问到这个类和对象， // 所以需要对外提供一个方法来获取这个唯一实例对象 // 注意：这个方法是否可以定义为singelton的一个成员方法呢？ // 答案是不能，因为如果为成员方法就必须要先访问对象、再访问函数 // 但是类和对象目前都已经私有化，外界无法访问，所以这个方法一定是一个全局普通函数 func GetInstance() *singelton { return instance } func (s *singelton) SomeThing() { fmt.Println(\u0026#34;单例对象的某方法\u0026#34;) } func main() { s := GetInstance() s.SomeThing() } 饿汉模式:\n在初始化单例唯一指针的时候，就已经提前开辟好了一个对象，申请了内存.\n饿汉式的好处是，不会出现线程并发创建，导致多个单例的出现，但是缺点是如果这个单例对象在业务逻辑没有被使用，也会客观的创建一块内存对象。那么与之对应的模式叫“懒汉式”\n单例模式 (懒汉模式)\npackage main import \u0026#34;fmt\u0026#34; type singelton struct { } var instance *singelton = new(singelton) func GetInstance() *singelton { // 如果对选哪个为空,创建一个新对象,否则不创建 if instance == nil { instance = new(singelton) return instance } return instance } func (i *singelton) Print() { fmt.Println(\u0026#34;Hello\u0026#34;) } func main(){ si := GetInstance() si.Print() } 懒汉模式虽然解决了饿汉模式的问题,但是也带来一个问题,就是如果有多个协程同时首次调用GetInstance()方法有概率导致多个实例被创建，则违背了单例的设计初衷。 由此我们可以进行加读写锁的操作来防止被创建多个实例.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) type singelton struct { } var lock sync.Mutex var instance *singelton = new(singelton) func GetInstance() *singelton { // 调用 GetInstance() 的时候加锁,防止别的协程使用 lock.Lock() defer lock.Unlock() // 如果对选哪个为空,创建一个新对象,否则不创建 if instance == nil { return new(singelton) } return instance } func (i *singelton) print() { fmt.Println(\u0026#34;Hello\u0026#34;) } func main(){ si := GetInstance() si.print() } 但是加上读写锁带来的问题就是,如果并发数量高了,会导致很多调用GetInstance()的协程被 一个读写锁阻塞,从而拖慢速度.\n线程安全的单例模式 # 所以接下来可以借助\u0026quot;sync/atomic\u0026quot;来进行内存的状态存留来做互斥。atomic就可以自动加载和设置标记.\n// sync/atomic\u0026#34;来进行内存的状态存留来做互斥 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;sync/atomic\u0026#34; ) type singelton struct { } var initialized uint32 var lock sync.Mutex var instance *singelton = new(singelton) func GetInstance() *singelton { //如果标记为被设置，直接返回，不加锁 if atomic.LoadUint32(\u0026amp;initialized) == 1 { return instance } lock.Lock() defer lock.Unlock() // 标记为0 创建一个新的,同时将标记设置为1.表示已经有实例 if initialized == 0{ instance = new(singelton) //设置标记位 atomic.StoreUint32(\u0026amp;initialized, 1) } return instance } func (i *singelton) print() { fmt.Println(\u0026#34;Hello\u0026#34;) } func main(){ si := GetInstance() si.print() } 当然也可以使用 sync 包中的 Once.Do() 方法来实现上面的操作\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) type singelton struct { } var once sync.Once var instance *singelton = new(singelton) func GetInstance() *singelton { once.Do(func () { instance = new(singelton) }) return instance } func (i *singelton) print() { fmt.Println(\u0026#34;Hello\u0026#34;) } func main(){ si := GetInstance() si.print() } sync.Once.Do 源码:\nfunc (o *Once) Do(f func()) { // Note: Here is an incorrect implementation of Do: // //\tif atomic.CompareAndSwapUint32(\u0026amp;o.done, 0, 1) { //\tf() //\t} // // Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first\u0026#39;s call to f to complete. // This is why the slow path falls back to a mutex, and why // the atomic.StoreUint32 must be delayed until after f returns. if atomic.LoadUint32(\u0026amp;o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) } } 优点：\n(1) 单例模式提供了对唯一实例的受控访问。\n(2) 节约系统资源。由于在系统内存中只存在一个对象。\n缺点：\n(1) 扩展略难。单例模式中没有抽象层。 (2) 单例类的职责过重。\n结构性模式 # 代理模式 # Proxy模式又叫做代理模式，是构造型的设计模式之一，它可以为其他对象提供一种代理（Proxy）以控制对这个对象的访问。\n所谓代理，是指具有与代理元（被代理的对象）具有相同的接口的类，客户端必须通过代理与被代理的目标类交互，而代理一般在交互的过程中（交互前后），进行某些特别的处理。\n用一个日常可见的案例来理解“代理”的概念，如下图： 这里假设有一个“自己”的角色，正在玩一款网络游戏。称这个网络游戏就是代理模式的“Subject”，表示要做一件事的目标或者对象事件主题。\n（1）“自己”有一个给游戏角色升级的需求或者任务，当然“自己”可以独自完成游戏任务的升级。\n（2）或者“自己”也可以邀请以为更加擅长游戏的“游戏代练”来完成升级这件事，这个代练就是“Proxy”代理。\n（3）“游戏代练”不仅能够完成升级的任务需求，还可以额外做一些附加的能力。比如打到一些好的游戏装备、加入公会等等周边收益。\n所以代理的出现实则是为了能够覆盖“自己”的原本的需求，且可以额外做其他功能，这种额外创建的类是不影响已有的“自己”和“网络游戏”的的关系。是额外添加，在设计模式原则上，是符合“开闭原则”思想。\nsubject（抽象主题角色）：真实主题与代理主题的共同接口。\nRealSubject（真实主题角色）：定义了代理角色所代表的真实对象。\nProxy（代理主题角色）： 含有对真实主题角色的引用，代理角色通常在将客户端调用传递给真是主题对象之前或者之后执行某些操作，而不是单纯返回真实的对象。 实例\n/* 代理模式 */ package main import ( \u0026#34;fmt\u0026#34; ) // 角色 type Character struct { ID string // 角色ID Level int // 等级 Banned bool // 是否被 } // Player 抽象层 type Player interface { UpLevel(character *Character) } // ChinaPlayer 实现层 type ChinaPlayer struct { Player } func (cp *ChinaPlayer) UpLevel(character *Character) { fmt.Println(\u0026#34;ChinaPlayer将\u0026#34;, character.ID, \u0026#34;的角色等级提升了100级\u0026#34;) character.Level = 100 fmt.Println(\u0026#34;现在角色等级为: \u0026#34;, character.Level) } type USPlayer struct { Player } func (up *USPlayer) UpLevel(character *Character) { fmt.Println(\u0026#34;US Player将\u0026#34;, character.ID, \u0026#34;的角色等级提升了100级\u0026#34;) character.Level = 100 fmt.Println(\u0026#34;现在角色等级为: \u0026#34;, character.Level) } // Booster 代理 type Booster struct { player Player // 代理某个主题 } func (bsr *Booster) UpLevel(character *Character) { // 先查看是否被封 if bsr.checkBanned(character) == false { bsr.player.UpLevel(character) // 调用原来的函数 bsr.Done(character) } } func MakeBooster(player Player) Player { return \u0026amp;Booster{player} } func (bsr *Booster) checkBanned(character *Character) bool { fmt.Println(\u0026#34;Scan \u0026#34;, character.ID) if character.Banned { fmt.Println(character.ID, \u0026#34;is Banned\u0026#34;) } return character.Banned } func (bsr *Booster) Done(character *Character) { fmt.Println(\u0026#34;Booster将\u0026#34;, character.ID, \u0026#34;的角色等级提升了100级\u0026#34;) character.Level = 100 fmt.Println(\u0026#34;现在角色等级为: \u0026#34;, character.Level) } func main() { ch1 := Character{ ID: \u0026#34;11\u0026#34;, Level: 10, Banned: false, } ch2 := Character{ ID: \u0026#34;22\u0026#34;, Level: 0, Banned: false, } ch3 := Character{ ID: \u0026#34;33\u0026#34;, Level: 0, Banned: true, } ch4 := Character{ ID: \u0026#34;44\u0026#34;, Level: 0, Banned: false, } // 不使用代理 var player Player player = new(ChinaPlayer) player.UpLevel(\u0026amp;ch1) player = new(ChinaPlayer) player.UpLevel(\u0026amp;ch2) // 使用代理模式 var boostPlayer Player boostPlayer = MakeBooster(player) boostPlayer.UpLevel(\u0026amp;ch3) boostPlayer.UpLevel(\u0026amp;ch4) } 输出结果\nChinaPlayer将 11 的角色等级提升了100级 现在角色等级为: 100 ChinaPlayer将 22 的角色等级提升了100级 现在角色等级为: 100 Scan 33 33 is Banned Scan 44 ChinaPlayer将 44 的角色等级提升了100级 现在角色等级为: 100 Booster将 44 的角色等级提升了100级 现在角色等级为: 100 代理模式的优缺点\n优点： (1) 能够协调调用者和被调用者，在一定程度上降低了系统的耦合度。 (2) 客户端可以针对抽象主题角色进行编程，增加和更换代理类无须修改源代码，符合开闭原则，系统具有较好的灵活性和可扩展性。\n缺点： (1) 代理实现较为复杂。\n装饰模式 # 装饰模式(Decorator Pattern)：动态地给一个对象增加一些额外的职责，就增加对象功能来说，装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。\nComponent（抽象构件）：它是具体构件和抽象装饰类的共同父类，声明了在具体构件中实现的业务方法，它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象，实现客户端的透明操作。\nConcreteComponent（具体构件）：它是抽象构件类的子类，用于定义具体的构件对象，实现了在抽象构件中声明的方法，装饰器可以给它增加额外的职责（方法）。\n和代理模式的区别在于 装饰器模式可以自由组合,而代理模式有内部逻辑无法自由组合\npackage main import \u0026#34;fmt\u0026#34; //--- 抽象层 ---- // Phone 手机抽象层 type Phone interface{ Show() } // 抽象装饰器 基础类 //(该类本应该为interface，但是Golang interface语法不可以有成员属性) type Decorator struct{ phone Phone } func (d *Decorator) Show(){ } // ---- 实体类层 ---- type Huawei struct{ } func (h *Huawei) Show(){ fmt.Println(\u0026#34;Huawei Phone\u0026#34;) } type Xiaomi struct{ } func (x *Xiaomi) Show(){ fmt.Println(\u0026#34;Xiaomi Phone\u0026#34;) } type MoDecorator struct { Decorator // 继承Decorator 主要方法 } func (x *MoDecorator) Show(){ x.phone.Show() fmt.Println(\u0026#34;Phone Mo\u0026#34;) } func NewMoDecorator(phone Phone) Phone{ return \u0026amp;MoDecorator{Decorator{phone}} } func main(){ hw := new(Huawei) hw.Show() xm := new(Xiaomi) xm.Show() md := NewMoDecorator(hw) md.Show() } 输出结果:\nHuawei Phone Xiaomi Phone Huawei Phone Phone Mo 优点：\n(1) 对于扩展一个对象的功能，装饰模式比继承更加灵活性，不会导致类的个数急剧增加。\n(2) 可以通过一种动态的方式来扩展一个对象的功能，从而实现不同的行为。\n(3) 可以对一个对象进行多次装饰。\n(4) 具体构件类与具体装饰类可以独立变化，用户可以根据需要增加新的具体构件类和具体装饰类，原有类库代码无须改变，符合“开闭原则”。\n缺点：\n(1) 使用装饰模式进行系统设计时将产生很多小对象，大量小对象的产生势必会占用更多的系统资源，影响程序的性能。\n(2) 装饰模式提供了一种比继承更加灵活机动的解决方案，但同时也意味着比继承更加易于出错，排错也很困难，对于多次装饰的对象，调试时寻找错误可能需要逐级排查，较为繁琐。\n适配器模式 # 适配器模式(Adapter Pattern) : 将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。\npackage main import \u0026#34;fmt\u0026#34; //适配的目标 type V5 interface { Use5V() } //业务类，依赖V5接口 type Phone struct { v V5 } func NewPhone(v V5) *Phone { return \u0026amp;Phone{v} } func (p *Phone) Charge() { fmt.Println(\u0026#34;Phone进行充电...\u0026#34;) p.v.Use5V() } //被适配的角色，适配者 type V220 struct {} func (v *V220) Use220V() { fmt.Println(\u0026#34;使用220V的电压\u0026#34;) } //电源适配器 type Adapter struct { v220 *V220 } func (a *Adapter) Use5V() { fmt.Println(\u0026#34;使用适配器进行充电\u0026#34;) //调用适配者的方法 a.v220.Use220V() } func NewAdapter(v220 *V220) *Adapter { return \u0026amp;Adapter{v220} } // ------- 业务逻辑层 ------- func main() { iphone := NewPhone(NewAdapter(new(V220))) iphone.Charge() } 输出结果:\nPhone进行充电... 使用适配器进行充电 使用220V的电压 其他实例\npackage main import \u0026#34;fmt\u0026#34; // 攻击接口 type Attack interface{ Fight() } // 英雄类 type Hero struct { Name string Attack Attack } func NewHero(name string,attack Attack) *Hero{ return \u0026amp;Hero{name,attack} } func (h *Hero) Do(){\tfmt.Println(h.Name,\u0026#34;Attack...\u0026#34;) h.Attack.Fight() } // 关机类 type PowerOff struct{ } func (po *PowerOff) ShutDown(){ fmt.Println(\u0026#34;Commputer ShutDown....\u0026#34;) } type Adapter struct{ powerOff *PowerOff } func (ap *Adapter) Fight(){ ap.powerOff.ShutDown() } func NewAdapter(powerOff *PowerOff) *Adapter{ return \u0026amp;Adapter{powerOff} } func main(){ hero := NewHero(\u0026#34;12313\u0026#34;,NewAdapter(new(PowerOff))) hero.Do() } 优点：\n(1) 将目标类和适配者类解耦，通过引入一个适配器类来重用现有的适配者类，无须修改原有结构。\n(2) 增加了类的透明性和复用性，将具体的业务实现过程封装在适配者类中，对于客户端类而言是透明的，而且提高了适配者的复用性，同一个适配者类可以在多个不同的系统中复用。\n(3) 灵活性和扩展性都非常好，可以很方便地更换适配器，也可以在不修改原有代码的基础上增加新的适配器类，完全符合“开闭原则”。\n缺点:\n适配器中置换适配者类的某些方法比较麻烦。\n外观模式 # 根据迪米特法则，如果两个类不必彼此直接通信，那么这两个类就不应当发生直接的相互作用。\nFacade模式也叫外观模式，是由GoF提出的23种设计模式中的一种。Facade模式为一组具有类似功能的类群，比如类库，子系统等等，提供一个一致的简单的界面。这个一致的简单的界面被称作facade。\npackage main import \u0026#34;fmt\u0026#34; type SubSystemA struct {} func (sa *SubSystemA) MethodA() { fmt.Println(\u0026#34;子系统方法A\u0026#34;) } type SubSystemB struct {} func (sb *SubSystemB) MethodB() { fmt.Println(\u0026#34;子系统方法B\u0026#34;) } type SubSystemC struct {} func (sc *SubSystemC) MethodC() { fmt.Println(\u0026#34;子系统方法C\u0026#34;) } type SubSystemD struct {} func (sd *SubSystemD) MethodD() { fmt.Println(\u0026#34;子系统方法D\u0026#34;) } //外观模式，提供了一个外观类， 简化成一个简单的接口供使用 type Facade struct { a *SubSystemA b *SubSystemB c *SubSystemC d *SubSystemD } func (f *Facade) MethodOne() { f.a.MethodA() f.b.MethodB() } func (f *Facade) MethodTwo() { f.c.MethodC() f.d.MethodD() } func main() { //如果不用外观模式实现MethodA() 和 MethodB() sa := new(SubSystemA) sa.MethodA() sb := new(SubSystemB) sb.MethodB() fmt.Println(\u0026#34;-----------\u0026#34;) //使用外观模式 f := Facade{ a: new(SubSystemA), b: new(SubSystemB), c: new(SubSystemC), d: new(SubSystemD), } //调用外观包裹方法 f.MethodOne() } 子系统方法A 子系统方法B ----------- 子系统方法A 子系统方法B 优点：\n(1) 它对客户端屏蔽了子系统组件，减少了客户端所需处理的对象数目，并使得子系统使用起来更加容易。通过引入外观模式，客户端代码将变得很简单，与之关联的对象也很少。\n(2) 它实现了子系统与客户端之间的松耦合关系，这使得子系统的变化不会影响到调用它的客户端，只需要调整外观类即可。\n(3) 一个子系统的修改对其他子系统没有任何影响。\n缺点：\n(1) 不能很好地限制客户端直接使用子系统类，如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。\n(2) 如果设计不当，增加新的子系统可能需要修改外观类的源代码，违背了开闭原则。\n行为型模式 # 模板方法模式 # 模板方法模式 在一个方法中定义一个算法的骨架，而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下，重新定义算法中的某些步骤。\nAbstractClass（抽象类）： 在抽象类中定义了一系列基本操作(PrimitiveOperations)，这些基本操作可以是具体的，也可以是抽象的，每一个基本操作对应算法的一个步骤，在其子类中可以重定义或实现这些步骤。同时，在抽象类中实现了一个模板方法(Template Method)，用于定义一个算法的框架，模板方法不仅可以调用在抽象类中实现的基本方法，也可以调用在抽象类的子类中实现的基本方法，还可以调用其他对象中的方法。\nConcreteClass（具体子类） 它是抽象类的子类，用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤，也可以覆盖在父类中已经实现的具体基本操作。\n抽象层\ntype Beverage interface { BoilWater() Brew() PourInCup() AddThings() } // 封装一套流程末班基类,让具体集成且实现 type Template struct { b Beverage } func (t *Template) MakeBeverage() { if t == nil { return } t.b.BoilWater() t.b.Brew() t.b.PourInCup() t.b.AddThings() } 实体类层\n// MakeCoffee集成模板 type MakeCoffee struct { Template } func (mc *MakeCoffee) BoilWater() { fmt.Println(\u0026#34;将水烧到 100 度\u0026#34;) } func (mc *MakeCoffee) Brew() { fmt.Println(\u0026#34;冲泡\u0026#34;) } func (mc *MakeCoffee) PourInCup() { fmt.Println(\u0026#34;导入杯中\u0026#34;) } func (mc *MakeCoffee) AddThings() { fmt.Println(\u0026#34;添加牛奶\u0026#34;) } func NewMakeCoffee() *MakeCoffee{ makeCoffee := new(MakeCoffee) makeCoffee.b = makeCoffee return makeCoffee } // MakeTea集成模板 type MakeTea struct{ Template } func (mt *MakeTea) BoilWater() { fmt.Println(\u0026#34;将水烧到 100 度\u0026#34;) } func (mt *MakeTea) Brew() { fmt.Println(\u0026#34;冲泡茶叶\u0026#34;) } func (mt *MakeTea) PourInCup() { fmt.Println(\u0026#34;导入杯中\u0026#34;) } func (mt *MakeTea) AddThings() { fmt.Println(\u0026#34;添加枸杞\u0026#34;) } func NewMakeTea() *MakeTea{ makeTea := new(MakeTea) makeTea.b = makeTea return makeTea } 业务层\nfunc main() { makeCoffee := NewMakeCoffee() makeCoffee.MakeBeverage() makeTea := NewMakeTea() makeTea.MakeBeverage() } 完整代码\npackage main import \u0026#34;fmt\u0026#34; type Beverage interface { BoilWater() Brew() PourInCup() AddThings() } // 封装一套流程末班基类,让具体集成且实现 type Template struct { b Beverage } func (t *Template) MakeBeverage() { if t == nil { return } t.b.BoilWater() t.b.Brew() t.b.PourInCup() t.b.AddThings() } // MakeCoffee集成模板 type MakeCoffee struct { Template } func (mc *MakeCoffee) BoilWater() { fmt.Println(\u0026#34;将水烧到 100 度\u0026#34;) } func (mc *MakeCoffee) Brew() { fmt.Println(\u0026#34;冲泡\u0026#34;) } func (mc *MakeCoffee) PourInCup() { fmt.Println(\u0026#34;导入杯中\u0026#34;) } func (mc *MakeCoffee) AddThings() { fmt.Println(\u0026#34;添加牛奶\u0026#34;) } func NewMakeCoffee() *MakeCoffee{ makeCoffee := new(MakeCoffee) makeCoffee.b = makeCoffee return makeCoffee } // MakeTea集成模板 type MakeTea struct{ Template } func (mt *MakeTea) BoilWater() { fmt.Println(\u0026#34;将水烧到 100 度\u0026#34;) } func (mt *MakeTea) Brew() { fmt.Println(\u0026#34;冲泡茶叶\u0026#34;) } func (mt *MakeTea) PourInCup() { fmt.Println(\u0026#34;导入杯中\u0026#34;) } func (mt *MakeTea) AddThings() { fmt.Println(\u0026#34;添加枸杞\u0026#34;) } func NewMakeTea() *MakeTea{ makeTea := new(MakeTea) makeTea.b = makeTea return makeTea } func main() { makeCoffee := NewMakeCoffee() makeCoffee.MakeBeverage() makeTea := NewMakeTea() makeTea.MakeBeverage() } 输出结果:\n将水烧到 100 度 冲泡 导入杯中 添加牛奶 将水烧到 100 度 冲泡茶叶 导入杯中 添加枸杞 优点：\n(1) 在父类中形式化地定义一个算法，而由它的子类来实现细节的处理，在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。\n(2) 模板方法模式是一种代码复用技术，它在类库设计中尤为重要，它提取了类库中的公共行为，将公共行为放在父类中，而通过其子类来实现不同的行为，它鼓励我们恰当使用继承来实现代码复用。\n(3) 可实现一种反向控制结构，通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。\n(4) 在模板方法模式中可以通过子类来覆盖父类的基本方法，不同的子类可以提供基本方法的不同实现，更换和增加新的子类很方便，符合单一职责原则和开闭原则。\n缺点：\n需要为每一个基本方法的不同实现提供一个子类，如果父类中可变的基本方法太多，将会导致类的个数增加，系统更加庞大，设计也更加抽象。\n命令模式 # Command（抽象命令类）： 抽象命令类一般是一个抽象类或接口，在其中声明了用于执行请求的execute()等方法，通过这些方法可以调用请求接收者的相关操作。\nConcreteCommand（具体命令类）： 具体命令类是抽象命令类的子类，实现了在抽象命令类中声明的方法，它对应具体的接收者对象，将接收者对象的动作绑定其中。在实现execute()方法时，将调用接收者对象的相关操作(Action)。\nInvoker（调用者）： 调用者即请求发送者，它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者，因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中，再调用具体命令对象的execute()方法，从而实现间接调用请求接收者的相关操作。\nReceiver（接收者）： 接收者执行与请求相关的操作，它具体实现对请求的业务处理。\npackage main import \u0026#34;fmt\u0026#34; // Receiver 接收者 type Receiver struct { } // Command 抽象的命令 type Command interface { execute() } // func (r *Receiver) action() { fmt.Println(\u0026#34;Command action\u0026#34;) } // ConcreteCommand type ConcreteCommand struct { receiver *Receiver } func (cc *ConcreteCommand) execute() { fmt.Println(\u0026#34;ConcreteCommand execute\u0026#34;) cc.receiver.action() } // Invoker 调度着 type Invoker struct { CmdList []Command } func (i *Invoker) Notify() { fmt.Println(\u0026#34;Invoker Notify\u0026#34;) if i.CmdList == nil { return } for _, cmd := range i.CmdList { cmd.execute() } } func main() { r := new(Receiver) i := new(Invoker) cmd := ConcreteCommand{r} i.CmdList = append(i.CmdList,\u0026amp;cmd) i.Notify() } 输出结果:\nInvoker Notify ConcreteCommand Execute Command Action 优点：\n(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用，因此请求者与接收者之间实现完全解耦，相同的请求者可以对应不同的接收者，同样，相同的接收者也可以供不同的请求者使用，两者之间具有良好的独立性。\n(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类，因此增加新的具体命令类很容易，无须修改原有系统源代码，甚至客户类代码，满足“开闭原则”的要求。\n(3) 可以比较容易地设计一个命令队列或宏命令（组合命令）。\n缺点：\n使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类，因此在某些系统中可能需要提供大量的具体命令类，这将影响命令模式的使用。\n策略模式 # Context（环境类）：环境类是使用算法的角色，它在解决某个问题（即实现某个方法）时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例，用于定义所采用的策略。\nStrategy（抽象策略类）：它为所支持的算法声明了抽象方法，是所有策略类的父类，它可以是抽象类或具体类，也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。\nConcreteStrategy（具体策略类）：它实现了在抽象策略类中声明的算法，在运行时，具体策略类将覆盖在环境类中定义的抽象策略类对象，使用一种具体的算法实现某个业务处理。\npackage main import \u0026#34;fmt\u0026#34; // 武器抽象策略 type WeaponStrategy interface { UseWeapon() } // 具体实现层 // 具体策略AK47 type AK47 struct{} func (ak *AK47) UseWeapon() { fmt.Println(\u0026#34;Use AK47\u0026#34;) } type Knife struct{} func (kf *Knife) UseWeapon() { fmt.Println(\u0026#34;Use Knife\u0026#34;) } // Person 使用策略 type Person struct { Strategy WeaponStrategy } func (p *Person) SetWeaponStrategy(s WeaponStrategy) { p.Strategy = s } func (p *Person) Fight() { p.Strategy.UseWeapon() } // 业务层 func main() { p := new(Person) p.SetWeaponStrategy(new(AK47)) p.Fight() p.SetWeaponStrategy(new(Knife)) p.Fight() } 输出结果:\nUse AK47 Use Knife 优点：\n(1) 策略模式提供了对“开闭原则”的完美支持，用户可以在不修改原有系统的基础上选择算法或行为，也可以灵活地增加新的算法或行为。\n(2) 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护，它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起，将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中，比直接继承环境类的办法还要原始和落后。\n(3) 策略模式提供了一种算法的复用机制。由于将算法单独提取出来封装在策略类中，因此不同的环境类可以方便地复用这些策略类。\n缺点：\n(1) 客户端必须知道所有的策略类，并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别，以便适时选择恰当的算法。换言之，策略模式只适用于客户端知道所有的算法或行为的情况。\n(2) 策略模式将造成系统产生很多具体策略类，任何细小的变化都将导致系统要增加一个新的具体策略类。\n观察者模式 # Subject（被观察者或目标，抽象主题）： 被观察的对象。当需要被观察的状态发生变化时，需要通知队列中所有观察者对象。Subject需要维持（添加，删除，通知）一个观察者对象的队列列表。\nConcreteSubject（具体被观察者或目标，具体主题）： 被观察者的具体实现。包含一些基本的属性状态及其他操作。\nObserver（观察者）： 接口或抽象类。当Subject的状态发生变化时，Observer对象将通过一个callback函数得到通知。\nConcreteObserver（具体观察者）： 观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。\npackage main import \u0026#34;fmt\u0026#34; // ======= 抽象层 ======= // Listener 抽象观察者 type Listener interface { Listen() } type Notifier interface { AddListener(listener Listener) RemoveListener(listener Listener) Notify() } // ======= 实现层 ======== // Listener01 观察者01 type Listener01 struct { Name string Action string } func (l1 *Listener01) DoAction() { fmt.Println(l1.Name, \u0026#34;Do Action\u0026#34;) } func (l1 *Listener01) Listen() { fmt.Println(l1.Name, \u0026#34;Stop Do\u0026#34;, l1.Action) } // Listener02 观察者02 type Listener02 struct { Name string Action string } func (l2 *Listener02) DoAction() { fmt.Println(l2.Name, \u0026#34;Do Action\u0026#34;) } func (l2 *Listener02) Listen() { fmt.Println(l2.Name, \u0026#34;Stop Do\u0026#34;, l2.Action) } // Notifier01 通知者 type Notifier01 struct { ListenerList []Listener // 观察者的数组 } func (n1 *Notifier01) AddListener(listener Listener) { n1.ListenerList = append(n1.ListenerList, listener) } func (n1 *Notifier01) RemoveListener(listener Listener) { for index, listen := range n1.ListenerList { // 找到要删除listener 的位置 if listener == listen { //将要删除的元素前后位置连起来 n1.ListenerList = append(n1.ListenerList[:index], n1.ListenerList[index+1:]...) break } } } func (n1 *Notifier01) Notify() { for _, listen := range n1.ListenerList { // 调用每个 listener listen.Listen() } } func main() { l1 := \u0026amp;Listener01{\u0026#34;L1\u0026#34;, \u0026#34;Action\u0026#34;} l2 := \u0026amp;Listener02{\u0026#34;L2\u0026#34;, \u0026#34;Action\u0026#34;} n1 := new(Notifier01) n1.AddListener(l1) n1.AddListener(l2) l1.DoAction() l2.DoAction() n1.Notify() n1.RemoveListener(l2) n1.RemoveListener(l1) } 输出结果:\nL1 Do Action L2 Do Action L1 Stop Do Action L2 Stop Do Action 优点：\n(1) 观察者模式可以实现表示层和数据逻辑层的分离，定义了稳定的消息更新传递机制，并抽象了更新接口，使得可以有各种各样不同的表示层充当具体观察者角色。\n(2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合，无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起，因此它们可以属于不同的抽象化层次。\n(3) 观察者模式支持广播通信，观察目标会向所有已注册的观察者对象发送通知，简化了一对多系统设计的难度。\n(4) 观察者模式满足“开闭原则”的要求，增加新的具体观察者无须修改原有系统代码，在具体观察者与观察目标之间不存在关联关系的情况下，增加新的观察目标也很方便。\n缺点：\n(1) 如果一个观察目标对象有很多直接和间接观察者，将所有的观察者都通知到会花费很多时间。\n(2) 如果在观察者和观察目标之间存在循环依赖，观察目标会触发它们之间进行循环调用，可能导致系统崩溃。\n(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的，而仅仅只是知道观察目标发生了变化。\n","date":"2023 年 03 月 27 日","externalUrl":null,"permalink":"/posts/golang-%E5%B8%B8%E7%94%A8%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/","section":"Posts","summary":"","title":"Golang 常用设计模式","type":"posts"},{"content":" 简介 # Zinx 是一个基于 Golang 的轻量级并发服务器框架\nGolang 轻量级并发服务器框架 zinx 文档)\n教程地址 :【zinx-Golang 轻量级 TCP 服务器框架(适合 Go 语言自学-深入浅出)】\n我自己的学习笔记: Xenolies/ZinxStudy: Golan-Zinx 框架学习 (github.com)\nZinx 架构 # Zinx v0.1 基础 Server # Zinx 框架有两个最基本的模块 Ziface 和 znet .\nziface 主要是存放一些 Zinx 框架的全部模块的抽象层接口类。\nZinx 框架的最基本的服务类接口 iserver，定义在ziface模块中。\nznet 模块是 Zinx 框架中网络相关功能的实现，所有网络相关模块定义在这里。\n定义 iserver 接口 # 服务接口有三个最基础功能,启动服务器,运行服务,还有停止服务.\n需要注意的是启动服务器可以写到运行服务中,来使用更方便\npackage ziface // IServer Server 定义一个服务器接口 type IServer interface { // Start 启动服务器 Start() // Stop 停止服务器 Stop() // Serve 运行服务 Serve() } 实例化 iserver 接口 # 定义了接口需要实例化接口,所以在 znet 下建立一个 server.go文件,将接口实例化\n这里编写了了个回显业务来返回客户端发送的数据.\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/Ziface\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) // Server 实现 IServer 接口 ,定义一个服务器模块 type Server struct { // 服务器名称 ServerName string // 服务器IP版本 IpVersion string // 服务器端口 IP string // 服务器监听端口 Port int } func (s *Server) Start() { fmt.Printf(\u0026#34;[START] Server Listener at IP: %s , Port %d is Starting\\n\u0026#34;, s.IP, s.Port) go func() { // 获取一个TCP的addr addr, err := net.ResolveTCPAddr(s.IpVersion, fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, s.IP, s.Port)) if err != nil { fmt.Println(\u0026#34;net.ResolveIPAddr Error : \u0026#34;, err) return } // 监听服务器地址 tcpListener, err := net.ListenTCP(s.IpVersion, addr) if err != nil { fmt.Println(\u0026#34;net.ListenTCP Error : \u0026#34;, err) return } fmt.Println(\u0026#34;Start Zinx Server Success! \u0026#34;, s.ServerName, \u0026#34;is Listening\u0026#34;) // 阻塞等待客户端链接和处理客户端链接业务(读写) for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(\u0026#34;tcpListener.AcceptTCP Error : \u0026#34;, err) continue } // 客户端链接后的读写操作 // 做一个最基本的512字节长度的回显业务 go func() { for { buf := make([]byte, 512) read, err := tcpConn.Read(buf) if err != nil { fmt.Println(\u0026#34;tcpConn.Read Error : \u0026#34;, err) continue } fmt.Printf(\u0026#34;Zinx Server Read: %s\\n\u0026#34;, buf[:read]) // 回显 if _, err := tcpConn.Write(buf[:read]); err != nil { fmt.Println(\u0026#34;tcpConn.Write Error: \u0026#34;, err) continue } } }() } }() } func (s *Server) Stop() { // 服务器终止 } func (s *Server) Serve() { // 启动服务 s.Start() // 建立阻塞状态 select {} } // NewServer 初始化 Server 模块 func NewServer(name string) Ziface.Server { s := \u0026amp;Server{ ServerName: name, IpVersion: \u0026#34;tcp4\u0026#34;, IP: \u0026#34;0.0.0.0\u0026#34;, Port: 8899, } return s } 回显业务测试 # 这里编写一个返回客户端发送数据的业务.\n然后创建一个目录名为TestDemo ,其下创建一个目录名为ClientDemo,\n在 ClientDemo目录下创建一个名为 Client.go ,模拟客户端输入.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;time\u0026#34;) /* 模拟客户端 */ func main() { fmt.Println(\u0026#34;Client Start...\u0026#34;) // 创建TCP连接,得到Conn连接 conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:8899\u0026#34;) if err != nil { fmt.Println(\u0026#34;net.Dial Error : \u0026#34;, err) return } for { // 调用Write写数据 _, err = conn.Write([]byte(\u0026#34;Hello,Zinx Server!\u0026#34;)) if err != nil { fmt.Println(\u0026#34;conn.Write Error : \u0026#34;, err) return } buf := make([]byte, 512) read, err := conn.Read(buf) if err != nil { fmt.Println(\u0026#34;conn.Read Error : \u0026#34;, err) return } fmt.Printf(\u0026#34;Server Back: %s\\n\u0026#34;, buf[:read]) time.Sleep(2 * time.Second) } } 接着在 TestDemo 创建一个目录 ServerDemo ,在 ServerDemo 目录下创建一个名为 Server.go 来建立 Zinx 服务.\n在TestDemo/ServerDemo/Server.go 中 写下如下代码:\npackage main import \u0026#34;ZinxDemo01/Zinx/znet\u0026#34; /** 基于 Zinx开发的服务端应用 */ func main() { s := znet.NewServer(\u0026#34;[Zinx0.1]\u0026#34;) //启动服务 s.Serve() } 然后启动服务器.\n此时服务器输出 :\n[START] Server Listener at IP: 0.0.0.0 , Port 8899 is Starting Start Zinx Server Success! [Zinx0.1] is Listening 说明服务器启动成功\n接着启动客户端服务,客户端向服务端发送 Hello,Zinx Server!\nClient Start... Server Back: Hello,Zinx Server! Server Back: Hello,Zinx Server! 此时服务器回显:\n[START] Server Listener at IP: 0.0.0.0 , Port 8899 is Starting Start Zinx Server Success! [Zinx0.1] is Listening Zinx Server Read: Hello,Zinx Server! Zinx Server Read: Hello,Zinx Server! Zinx Server Read: Hello,Zinx Server! Zinx Server Read: Hello,Zinx Server! Zinx Server Read: Hello,Zinx Server! Zinx 框架 v0.1 基础的 Server 模块搭建成功\nZinx v0.2 链接封装与业务绑定 # 链接接口封装 # 一个客户端就要使用一个匿名函数处理是完全不够用的 ,所以需要定义 Connection (链接) 模块来实现对于客户端链接的处理, 首先创建一个 IConnection.go 接着编写代码定义一个接口.\npackage Ziface import \u0026#34;net\u0026#34; // Connection 定义连接模块接口 type Connection interface { // Start 启动连接 让当前连接准备开始工作 Start() // Stop 停止连接 结束当前连接的工作 Stop() // GetTCPConnection GetTCPConnetion 获取当前链接绑定的 Socket Conn GetTCPConnection() *net.TCPConn // GetConnID 获取当前连接模块的ID GetConnID() uint32 // RemoteAddr 获取远程客户端连接的TCP状态 RemoteAddr() net.Addr // Send 发送数据 将数据发送给远程的客户端 Send(data []byte) error } // HandleFunc 定义一个处理业务的方法 type HandleFunc func(*net.TCPConn, []byte, int) error 接着在 znet 目录创建一个名为 Connection.go 的文件来实现接口.\npackage znet import ( \u0026#34;net\u0026#34; ) // Connection 当前连接的模块 type Connection struct { // 当前连接的Socket TCP 套接字 Conn *net.TCPConn // 当前连接的ID ConnID uint32 // 当前连接的状态 isClosed bool // 当前连接的绑定的处理业务的方法 handleAPI Ziface.HandleFunc // 告知当前连接退出的Channel ExitChan chan bool } // NewConnection 初始化连接模块的方法 func NewConnection(conn *net.TCPConn, connID uint32, callbackAPI Ziface.HandleFunc) *Connection { c := \u0026amp;Connection{ Conn: conn, ConnID: connID, handleAPI: callbackAPI, isClosed: false, ExitChan: make(chan bool, 1), } return c } 业务绑定 # 光实现有个接口和简单的初始化是没用的, 所以需要编写相关业务\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/Ziface\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34;) // Connection 当前连接的模块 type Connection struct { // 当前连接的Socket TCP 套接字 Conn *net.TCPConn // 当前连接的ID ConnID uint32 // 当前连接的状态 isClosed bool // 当前连接的绑定的处理业务的方法 handleAPI Ziface.HandleFunc // 告知当前连接退出的Channel ExitChan chan bool } // NewConnection 初始化连接模块的方法 func NewConnection(conn *net.TCPConn, connID uint32, callbackAPI Ziface.HandleFunc) *Connection { c := \u0026amp;Connection{ Conn: conn, ConnID: connID, handleAPI: callbackAPI, isClosed: false, ExitChan: make(chan bool, 1), } return c } // StartReader 连接读的业务 func (c *Connection) StartReader() { fmt.Println(\u0026#34;Reader Goroutine is Running....\u0026#34;) defer fmt.Printf(\u0026#34;ConnID: %d Reader is Exit, Remote Addr is : %s\u0026#34;, c.ConnID, c.RemoteAddr().String()) defer c.Stop() for { // 建立阻塞读取客户端数据到buf中 buf := make([]byte, 512) read, err := c.Conn.Read(buf) if err != nil { fmt.Printf(\u0026#34;c.Conn.Read Error: %s\\n\u0026#34;, err) continue } // 调用当前连接绑定的HandleAPI if err := c.handleAPI(c.Conn, buf, read); err != nil { fmt.Printf(\u0026#34;c.ConnID: %d , Handle Error: %s\\n\u0026#34;, c.ConnID, err) break } } } // Start 启动连接 让当前连接准备开始工作 func (c *Connection) Start() { fmt.Println(\u0026#34;Connection START...\u0026#34;) // 启动当前连接读数据的业务 go c.StartReader() } // Stop 停止连接 结束当前连接的工作 func (c *Connection) Stop() { fmt.Printf(\u0026#34;Connection STOP.... , ConnID: %d\\n\u0026#34;, c.ConnID) //如果当前连接已经关闭 if c.isClosed { return } c.isClosed = true c.Conn.Close() close(c.ExitChan) } // GetTCPConnection GetTCPConnetion 获取当前链接绑定的 Socket Connfunc (c *Connection) GetTCPConnection() *net.TCPConn { return c.Conn } // GetConnID 获取当前连接模块的ID func (c *Connection) GetConnID() uint32 { return c.ConnID } // RemoteAddr 获取远程客户端连接的TCP状态 func (c *Connection) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() } // Send 发送数据 将数据发送给远程的客户端 func (c *Connection) Send(data []byte) error { return nil } 由于有了 Connection 模块,对于客户端的链接可以交给专门的方法了,所以要对 Server 做一些改动. 删除原来用于处理链接回显的 匿名函数,改为链接接口实现里面的方法. 同时也要防止出现冲突,把写一个自增定义唯一一个链接 ID.\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/Ziface\u0026#34; \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34;) // Server 实现 IServer 接口 ,定义一个服务器模块 type Server struct { // 服务器名称 ServerName string // 服务器IP版本 IpVersion string // 服务器端口 IP string // 服务器监听端口 Port int } // CallBackToClient 定义当前客户端连接的 Handle API 目前这个 Handle 写死,可以用户自己优化 func CallBackToClient(conn *net.TCPConn, buf []byte, read int) error { // 回显业务 fmt.Println(\u0026#34;[ConnHandle] CallBackToClient ...\u0026#34;) _, err := conn.Write(buf[:read]) if err != nil { fmt.Println(\u0026#34;conn.Write CallBackToClient Error: \u0026#34;, err) return errors.New(\u0026#34;CallBack\u0026#34;) } return nil } func (s *Server) Start() { fmt.Printf(\u0026#34;[START] Server Listener at IP: %s , Port %d is Starting\\n\u0026#34;, s.IP, s.Port) go func() { // 获取一个TCP的addr addr, err := net.ResolveTCPAddr(s.IpVersion, fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, s.IP, s.Port)) if err != nil { fmt.Println(\u0026#34;net.ResolveIPAddr Error : \u0026#34;, err) return } // 监听服务器地址 tcpListener, err := net.ListenTCP(s.IpVersion, addr) if err != nil { fmt.Println(\u0026#34;net.ListenTCP Error : \u0026#34;, err) return } var conId uint32 conId = 0 fmt.Println(\u0026#34;Start Zinx Server Success! \u0026#34;, s.ServerName, \u0026#34;is Listening\u0026#34;) // 阻塞等待客户端链接和处理客户端链接业务(读写) for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(\u0026#34;tcpListener.AcceptTCP Error : \u0026#34;, err) continue } // 客户端链接后的读写操作 // 将处理新链接的任务方法和Conn绑定得到连接模块 Conn := NewConnection(tcpConn, conId, CallBackToClient) conId++ //启动连接任务处理 go Conn.Start() } }() } func (s *Server) Stop() { // 服务器终止 } func (s *Server) Serve() { // 启动服务 s.Start() // 建立阻塞状态 select {} } // NewServer 初始化 Server 模块 func NewServer(name string) Ziface.Server { s := \u0026amp;Server{ ServerName: name, IpVersion: \u0026#34;tcp4\u0026#34;, IP: \u0026#34;0.0.0.0\u0026#34;, Port: 8899, } return s } 运行测试 # 服务器端\n[START] Server Listener at IP: 0.0.0.0 , Port 8899 is Starting Start Zinx Server Success! [Zinx] is Listening Connection START... Reader Goroutine is Running.... [ConnHandle] CallBackToClient ... [ConnHandle] CallBackToClient ... [ConnHandle] CallBackToClient ... [ConnHandle] CallBackToClient ... [ConnHandle] CallBackToClient ... 可以看到客户端的链接被 Connection 模块处理了\n客户端\nClient Start... Server Back: Hello,Zinx Server! Server Back: Hello,Zinx Server! Server Back: Hello,Zinx Server! Server Back: Hello,Zinx Server! Zinx v0.2 链接封装与业务绑定完成\nZinx v0.3 框架路由模块 # 编写抽象接口 # IRequest 消息请求抽象类:\npackage ziface // IRequest 接口 将客户端请求的练级信息和请求数据封装到一个Request中 type IRequest interface { // GetConnection 得到当前链接 GetConnection() IConnection // GetData 得到请求的消息 GetData([]byte) []byte } IRouter 路由配置抽象类：\npackage ziface // IRouter 路由抽象接口 路由里面数据都是IRequest type IRouter interface { // PreHandle 处理Conn业务之前的钩子方法 Hook PreHandle(request IRequest) // Handle 处理 Conn业务的主方法 Hook Handle(request IRequest) // PostHandle 处理Conn 业务之后的钩子方法 Hook PostHandle(request IRequest) } 实例化接口 # 实例化 IRouter 这里定义一个 BaseRouter 是为了以后扩展路由,客户可以自定义路由.\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; ) // BaseRouter 实现 Router时,线嵌入 BaseRouter基类.然后根据需要对这个基类进行重写 // 实现接口隔离 type BaseRouter struct { } // PreHandle 处理Conn业务之前的钩子方法 Hook func (br *BaseRouter) PreHandle(request ziface.IRequest) { } // Handle 处理 Conn业务的主方法 Hook func (br *BaseRouter) Handle(request ziface.IRequest) { } // PostHandle 处理Conn 业务之后的钩子方法 Hook func (br *BaseRouter) PostHandle(request ziface.IRequest) { } 实例化 IRequest 接口\npackage znet import \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; type Request struct { // 已经和客户端建立好的链接 conn ziface.IConnection //客户端请求的数据 data []byte } // GetConnection 获取客户端链接 func (r *Request) GetConnection() ziface.IConnection { return r.conn } // GetData 获取用户端请求的数据 func (r *Request) GetData(data []byte) []byte { return r.data } 集成路由模块 # 在 Connection 模块中添加一个新字段 Router ziface.IRouter 来集成路由模块.\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34;) // Connection 当前连接的模块 type Connection struct { // 当前连接的Socket TCP 套接字 Conn *net.TCPConn // 当前连接的ID ConnID uint32 // 当前连接的状态 isClosed bool // 告知当前连接退出的Channel ExitChan chan bool // 当前连接的Router处理 Router ziface.IRouter } // NewConnection 初始化连接模块的方法 func NewConnection(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection { c := \u0026amp;Connection{ Conn: conn, ConnID: connID, isClosed: false, ExitChan: make(chan bool, 1), Router: router, } return c } // StartReader 连接读的业务 func (c *Connection) StartReader() { fmt.Println(\u0026#34;Reader Goroutine is Running....\u0026#34;) defer fmt.Printf(\u0026#34;ConnID: %d Reader is Exit, Remote Addr is : %s\u0026#34;, c.ConnID, c.RemoteAddr().String()) defer c.Stop() for { // 建立阻塞读取客户端数据到buf中 buf := make([]byte, 512) _, err := c.Conn.Read(buf) if err != nil { fmt.Printf(\u0026#34;c.Conn.Read Error: %s\\n\u0026#34;, err) continue } // 得到当前Conn数据的Request的请求数据 req := Request{ conn: c, data: buf, } // 预注册路由方法 go func(request ziface.IRequest) { c.Router.PreHandle(request) c.Router.Handle(request) c.Router.PostHandle(request) }(\u0026amp;req) //在路由中找到注册绑定的Conn的Router调用 } } // Start 启动连接 让当前连接准备开始工作 func (c *Connection) Start() { fmt.Println(\u0026#34;Connection START...\u0026#34;) // 启动当前连接读数据的业务 go c.StartReader() } // Stop 停止连接 结束当前连接的工作 func (c *Connection) Stop() { fmt.Printf(\u0026#34;Connection STOP.... , ConnID: %d\\n\u0026#34;, c.ConnID) //如果当前连接已经关闭 if c.isClosed { return } c.isClosed = true c.Conn.Close() close(c.ExitChan) } // GetTCPConnection GetTCPConnection 获取当前链接绑定的 Socket Conn func (c *Connection) GetTCPConnection() *net.TCPConn { return c.Conn } // GetConnID 获取当前连接模块的ID func (c *Connection) GetConnID() uint32 { return c.ConnID } // RemoteAddr 获取远程客户端连接的TCP状态 func (c *Connection) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() } // Send 发送数据 将数据发送给远程的客户端 func (c *Connection) Send(data []byte) error { return nil } 之后在 Server 中加入添加路由的方法 ,并且 将原来处理 Handle 的方法交给路由.\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34;) // Server 实现 IServer 接口 ,定义一个服务器模块 type Server struct { // 服务器名称 ServerName string // 服务器IP版本 IpVersion string // 服务器端口 IP string // 服务器监听端口 Port int // 当前Sever添加一个Router,Server注册的链接处理业务 Router ziface.IRouter } func (s *Server) Start() { fmt.Printf(\u0026#34;[START] Server Listener at IP: %s , Port %d is Starting\\n\u0026#34;, s.IP, s.Port) go func() { // 获取一个TCP的addr addr, err := net.ResolveTCPAddr(s.IpVersion, fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, s.IP, s.Port)) if err != nil { fmt.Println(\u0026#34;net.ResolveIPAddr Error : \u0026#34;, err) return } // 监听服务器地址 tcpListener, err := net.ListenTCP(s.IpVersion, addr) if err != nil { fmt.Println(\u0026#34;net.ListenTCP Error : \u0026#34;, err) return } var conId uint32 conId = 0 fmt.Println(\u0026#34;Start Zinx Server Success! \u0026#34;, s.ServerName, \u0026#34;is Listening\u0026#34;) // 阻塞等待客户端链接和处理客户端链接业务(读写) for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(\u0026#34;tcpListener.AcceptTCP Error : \u0026#34;, err) continue } // 客户端链接后的读写操作 // 将处理新链接的任务方法和Conn绑定得到连接模块 dealConn := NewConnection(tcpConn, conId, s.Router) conId++ //启动连接任务处理 go dealConn.Start() } }() } func (s *Server) Stop() { // 服务器终止 } func (s *Server) Serve() { // 启动服务 s.Start() // 建立阻塞状态 select {} } // NewServer 初始化 Server 模块 func NewServer(name string) ziface.IServer { s := \u0026amp;Server{ ServerName: name, IpVersion: \u0026#34;tcp4\u0026#34;, IP: \u0026#34;0.0.0.0\u0026#34;, Port: 8899, Router: nil, } return s } func (s *Server) AddRouter(router ziface.IRouter) { s.Router = router fmt.Println(\u0026#34;Router Add Success!!\u0026#34;) } 接着在 IServer 接口中添加 AddRouter 方法\npackage ziface // IServer Server 定义一个服务器接口 type IServer interface { // Start 启动服务器 Start() // Stop 运行服务器 Stop() // Serve 运行服务 Serve() // AddRouter 路由功能 给当前服务注册一个 路由,来处理客户端链接 AddRouter(router IRouter) } 编写自定义路由 # 这次转到客户层 TestDemo/ServerDemo/Server.go 编写自定义路由.处理 Conn 业务之前,Conn 业务和之后.\npackage main import ( \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; \u0026#34;ZinxDemo01/Zinx/znet\u0026#34; \u0026#34;fmt\u0026#34;) /** 基于 Zinx开发的服务端应用 */ func main() { s := znet.NewServer(\u0026#34;[Zinx]\u0026#34;) // 当前 Zinx 框架添加 Router s.AddRouter(\u0026amp;PingRouter{}) s.Serve() } // PingRouter 自定义路由 type PingRouter struct { znet.BaseRouter } // PreHandle 测试路由 func (pr *PingRouter) PreHandle(request ziface.IRequest) { fmt.Println(\u0026#34;Call Router PreHandle...\u0026#34;) _, err := request.GetConnection().GetTCPConnection().Write([]byte(\u0026#34;Before Ping.... | \u0026#34;)) if err != nil { fmt.Println(\u0026#34;Router PreHandle Write Error: \u0026#34;, err) } } // Handle 测试路由 func (pr *PingRouter) Handle(request ziface.IRequest) { fmt.Println(\u0026#34;Call Router Handle...\u0026#34;) _, err := request.GetConnection().GetTCPConnection().Write([]byte(\u0026#34;....Ping....Ping....Ping.... | \u0026#34;)) if err != nil { fmt.Println(\u0026#34;Router Handle Write Error: \u0026#34;, err) } } // PostHandle 测试路由 func (pr *PingRouter) PostHandle(request ziface.IRequest) { fmt.Println(\u0026#34;Call Router PostHandle...\u0026#34;) _, err := request.GetConnection().GetTCPConnection().Write([]byte(\u0026#34;After Ping....\u0026#34;)) if err != nil { fmt.Println(\u0026#34;Router PostHandle Write Error: \u0026#34;, err) } } 客户端不变\n启动 Zinx 服务器\n服务端\nRouter Add Success!! [START] Server Listener at IP: 0.0.0.0 , Port 8899 is Starting Start Zinx Server Success! [Zinx] is Listening Connection START... Reader Goroutine is Running.... Call Router PreHandle... Call Router Handle... Call Router PostHandle... Call Router PreHandle... Call Router Handle... Call Router PostHandle... Call Router PreHandle... Call Router Handle... Call Router PostHandle... 服务端\nClient Start... Server Back: Before Ping.... | Server Back: ....Ping....Ping....Ping.... | After Ping.... Server Back: Before Ping.... | ....Ping....Ping....Ping.... | After Ping....Before Ping.... | ....Ping....Ping....Ping.... | After Ping.... Server Back: Before Ping.... | Server Back: ....Ping....Ping....Ping.... | After Ping.... Server Back: Before Ping.... | ....Ping....Ping....Ping.... | After Ping.... 这里看到返回的并不是连续的,其实这里遇到了 TCP 粘包的问题.\n**TCP粘包出现的原因** : 主要原因就是 TCP 数据传递模式是流模式，在保持长连接的时候可以进行多次的收和发。\n1.由 Nagle 算法造成的发送端的粘包：Nagle 算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给 TCP 发送时，TCP 并不立刻发送此段数据，而是等待一小段时间看看在等待期间是否还有要发送的数据，若有则会一次把这两段数据发送出去。\n2.接收端接收不及时造成的接收端粘包：TCP 会把接收到的数据存在自己的缓冲区中，然后通知应用层取数据。当应用层由于某些原因不能及时的把 TCP 的数据取出来，就会造成 TCP 缓冲区中存放了几段数据。\nZinx v0.4 全局配置文件编写 # 编写全局配置模块 # 在 Zinx下创建一个组件目录 utils ,然后在 utils 目录下创建一个全局配置组件 globalObj.go ,编写全局配置模块.\npackage utils import ( \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;os\u0026#34;) /* 全局配置模块 在 \u0026#34;服务器程序/conf/zinx.json\u0026#34;中写入配置 将框架中的硬代码.使用 globalObj进行替换 */ type GlobalObj struct { /* Server Info */ TcpServer ziface.IServer // 当前 Zinx Server 全局对选哪个 Host string // 当前服务器监听IP TcpPort int // 当前服务器端口 Name string // 按当前服务器名称 /* Zinx Info */ Version string // 当前 Zinx 版本号 MaxConn int // 最大链接数量 MaxPackageSize uint32 // 当前 Zinx 数据包最大值 } var GlobalObject *GlobalObj // 提供一个 init 方法,提供一个初始的默认值 func init() { GlobalObject = \u0026amp;GlobalObj{ TcpServer: nil, Host: \u0026#34;0.0.0.0\u0026#34;, TcpPort: 8899, Name: \u0026#34;Zinx Server\u0026#34;, Version: \u0026#34;v0.4\u0026#34;, MaxConn: 10, MaxPackageSize: 512, } // 从 conf/zinx.json 加载用户自定义参数 GlobalObject.Reload() // } // Reload 从 zinx.json中加载自定义参数 func (g *GlobalObj) Reload() { data, err := os.ReadFile(\u0026#34;conf/zinx.json\u0026#34;) if err != nil { panic(err) } // 将 JSON 文件解析到 GlobalObj err = json.Unmarshal(data, \u0026amp;GlobalObject) if err != nil { return } } 全局参数硬代码替换 # 将之前写死的参数使用全局变量 GlobalObject 替换\nServer.go\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/utils\u0026#34; \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34;) // Server 实现 IServer 接口 ,定义一个服务器模块 type Server struct { // 服务器名称 ServerName string // 服务器IP版本 IpVersion string // 服务器端口 IP string // 服务器监听端口 Port int // 当前Sever添加一个Router,Server注册的链接处理业务 Router ziface.IRouter } func (s *Server) Start() { fmt.Printf(\u0026#34;[START GlobalObject] Server Listener at IP: %s , Port %d is Starting\\n\u0026#34;, utils.GlobalObject.Host, utils.GlobalObject.TcpPort) fmt.Printf(\u0026#34;[START] Server Listener at IP: %s , Port %d is Starting\\n\u0026#34;, s.IP, s.Port) go func() { // 获取一个TCP的addr addr, err := net.ResolveTCPAddr(s.IpVersion, fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, s.IP, s.Port)) if err != nil { fmt.Println(\u0026#34;net.ResolveIPAddr Error : \u0026#34;, err) return } // 监听服务器地址 tcpListener, err := net.ListenTCP(s.IpVersion, addr) if err != nil { fmt.Println(\u0026#34;net.ListenTCP Error : \u0026#34;, err) return } var conId uint32 conId = 0 fmt.Println(\u0026#34;Start Zinx Server Success! [\u0026#34;, utils.GlobalObject.Name, \u0026#34;] is Listening\u0026#34;) fmt.Println(\u0026#34;Start Zinx Server Success! \u0026#34;, s.ServerName, \u0026#34; is Listening\u0026#34;) // 阻塞等待客户端链接和处理客户端链接业务(读写) for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(\u0026#34;tcpListener.AcceptTCP Error : \u0026#34;, err) continue } // 客户端链接后的读写操作 // 将处理新链接的任务方法和Conn绑定得到连接模块 dealConn := NewConnection(tcpConn, conId, s.Router) conId++ //启动连接任务处理 go dealConn.Start() } }() } func (s *Server) Stop() { // 服务器终止 } func (s *Server) Serve() { // 启动服务 s.Start() // 建立阻塞状态 select {} } // NewServer 初始化 Server 模块 func NewServer(name string) ziface.IServer { s := \u0026amp;Server{ // 使用 utils.GlobalObject 替换 ServerName: utils.GlobalObject.Name, IpVersion: \u0026#34;tcp4\u0026#34;, IP: utils.GlobalObject.Host, Port: utils.GlobalObject.TcpPort, Router: nil, } return s } func (s *Server) AddRouter(router ziface.IRouter) { s.Router = router fmt.Println(\u0026#34;Router Add Success!!\u0026#34;) } Connection.go\npackage znet import ( \u0026#34;ZinxDemo01/Zinx/utils\u0026#34; \u0026#34;ZinxDemo01/Zinx/ziface\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34;) // Connection 当前连接的模块 type Connection struct { // 当前连接的Socket TCP 套接字 Conn *net.TCPConn // 当前连接的ID ConnID uint32 // 当前连接的状态 isClosed bool // 告知当前连接退出的Channel ExitChan chan bool // 当前连接的Router处理 Router ziface.IRouter } // NewConnection 初始化连接模块的方法 func NewConnection(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection { c := \u0026amp;Connection{ Conn: conn, ConnID: connID, isClosed: false, ExitChan: make(chan bool, 1), Router: router, } return c } // StartReader 连接读的业务 func (c *Connection) StartReader() { fmt.Println(\u0026#34;Reader Goroutine is Running....\u0026#34;) defer fmt.Printf(\u0026#34;ConnID: %d Reader is Exit, Remote Addr is : %s\u0026#34;, c.ConnID, c.RemoteAddr().String()) defer c.Stop() for { // 建立阻塞读取客户端数据到buf中 buf := make([]byte, utils.GlobalObject.MaxPackageSize) _, err := c.Conn.Read(buf) if err != nil { fmt.Printf(\u0026#34;c.Conn.Read Error: %s\\n\u0026#34;, err) continue } // 得到当前Conn数据的Request的请求数据 req := Request{ conn: c, data: buf, } // 预注册路由方法 go func(request ziface.IRequest) { c.Router.PreHandle(request) c.Router.Handle(request) c.Router.PostHandle(request) }(\u0026amp;req) //在路由中找到注册绑定的Conn的Router调用 } } // Start 启动连接 让当前连接准备开始工作 func (c *Connection) Start() { fmt.Println(\u0026#34;Connection START...\u0026#34;) // 启动当前连接读数据的业务 go c.StartReader() } // Stop 停止连接 结束当前连接的工作 func (c *Connection) Stop() { fmt.Printf(\u0026#34;Connection STOP.... , ConnID: %d\\n\u0026#34;, c.ConnID) //如果当前连接已经关闭 if c.isClosed { return } c.isClosed = true c.Conn.Close() close(c.ExitChan) } // GetTCPConnection GetTCPConnection 获取当前链接绑定的 Socket Conn func (c *Connection) GetTCPConnection() *net.TCPConn { return c.Conn } // GetConnID 获取当前连接模块的ID func (c *Connection) GetConnID() uint32 { return c.ConnID } // RemoteAddr 获取远程客户端连接的TCP状态 func (c *Connection) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() } // Send 发送数据 将数据发送给远程的客户端 func (c *Connection) Send(data []byte) error { return nil } 开发服务器应用 # 首先在TestDemo\\ServerDemo 下建立 conf/zinx.json 的文件.写上Demo配置\n{ \u0026#34;Name\u0026#34;: \u0026#34;Zinx v0.4 Demo\u0026#34;, \u0026#34;Host\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;TcpPort\u0026#34;: \u0026#34;8899\u0026#34;, \u0026#34;MaxConn\u0026#34;: \u0026#34;3\u0026#34;, \u0026#34;MaxPackageSize\u0026#34;: \u0026#34;512\u0026#34; } 然后开启终端,使用 go run 命令启动服务器 ,可以看到服务端读取配置文件的结果\nPS D:\\Zinx-Study\\ZinxStudy\\TestDemo\\ServerDemo\u0026gt; go run Server.go Router Add Success!! [START GlobalObject] Server Listener at IP: 127.0.0.1 , Port 8899 is Starting [START] Server Listener at IP: 127.0.0.1 , Port 8899 is Starting Start Zinx Server Success! [ Zinx v0.4 Demo ] is Listening Start Zinx Server Success! Zinx v0.4 Demo is Listening Connection START... Reader Goroutine is Running.... Zinx v0.5 消息封装模块 # ","date":"2023 年 03 月 10 日","externalUrl":null,"permalink":"/posts/golang-zinx-%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/","section":"Posts","summary":"","title":"Golang Zinx 框架入门","type":"posts"},{"content":"","date":"2023 年 03 月 07 日","externalUrl":null,"permalink":"/tags/c%23/","section":"Tags","summary":"","title":"C#","type":"tags"},{"content":" 简介 # C#（读作“See Sharp”）是一种新式编程语言，不仅面向对象，还类型安全。 开发人员利用 C# 能够生成在 .NET 中运行的多种安全可靠的应用程序。\nC# 源于 C 语言系列，C、C++、Java 和 JavaScript 程序员很快就可以上手使用。\n下面列出了 C# 成为一种广泛应用的专业语言的原因：\n现代的、通用的编程语言。 面向对象。 面向组件。 容易学习。 结构化语言。 它产生高效率的程序。 它可以在多种计算机平台上编译。 .Net 框架的一部分。 环境搭建 # .NET 环境 # 安装 .NET Core SDK 下载 .NET(Linux、macOS 和 Windows) 下载安装完成后, 进入任务管理器，win + R 打开运行窗体 输入 cmd\n输入指令 ： dotnet --info 输出如下环境信息则表示搭建成功.\n.NET SDK: Version: 7.0.201 Commit: 68f2d7e7a3 运行时环境: OS Name: Windows OS Version: 10.0.19045 OS Platform: Windows RID: win10-x64 Base Path: C:\\Program Files\\dotnet\\sdk\\7.0.201\\ Host: Version: 7.0.3 Architecture: x64 Commit: 0a2bda10e8 .NET SDKs installed: 7.0.201 [C:\\Program Files\\dotnet\\sdk] .NET runtimes installed: Microsoft.AspNetCore.App 7.0.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App] Microsoft.NETCore.App 7.0.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 7.0.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App] Other architectures found: x86 [C:\\Program Files (x86)\\dotnet] registered at [HKLM\\SOFTWARE\\dotnet\\Setup\\InstalledVersions\\x86\\InstallLocation] Environment variables: Not set global.json file: Not found Learn more: https://aka.ms/dotnet/info Download .NET: https://aka.ms/dotnet/download 如果输入后发现显示的是 dotnet不是内部或外部的命令，也不是可运行的程序或批处理文件 需要手动添加环境变量. 添加内容如下: 在“系统变量”部分选中 Path，并点击编辑 添加如下路径:\nC:\\Windows; C:\\Windows\\System32\\System32\\Wbem; C:\\Windows\\System32; C:\\Program Files\\dotnet; C:\\Windows\\System32\\WindowsPowerShell\\V1.0; 接着点击确定保存,\n重新打开 CMD 窗口 输入 dotnet --info\nHello,World # 打开 CMD 窗口 ,输入 dotnet new console -o testObj\n创建一个名为 testObj的控制台程序,如果创建成功,控制台会输出如下信息\n$ dotnet new console -o testObj 已成功创建模板“控制台应用”。 正在处理创建后操作... 正在还原 C:\\Users\\35367\\Desktop\\testObj\\testObj.csproj: 正在确定要还原的项目… 已还原 C:\\Users\\35367\\Desktop\\testObj\\testObj.csproj (用时 114 ms)。 已成功还原。 然后找到项目路径下的 Program.cs 文件,使用 IDE 或者编辑器将文件内容改为下面的:\nusing System; namespace HelloWorldApplication { class HelloWorld { static void Main(string[] args) { Console.WriteLine(\u0026#34;Hello World\u0026#34;); Console.ReadKey(); } } } 接着将控制台移动到你的项目目录下.输入 dotnet run Program.cs 如果输出 Hello World! 说明 C# 环境搭建成功.\nC# 的 IDE # IDE 可以使用微软官方的 Visual Studio.\nVisual Studio 2022 社区版 - 下载最新的免费版本 或者是 Visual Studio Code .\nVisual Studio Code - Code Editing. Redefined\n当然也可以使用 JetBrains 的 Rider\nRider：JetBrains 出品的跨平台 .NET IDE\n因为我学生资格还在,所以用的是 Rider .\n程序结构 # 一个 C# 程序主要包括以下部分：\n命名空间声明（Namespace declaration） 一个 class Class 方法 Class 属性 一个 Main 方法 语句（Statements）\u0026amp; 表达式（Expressions） 注释 C# 文件的后缀为 .cs\n一个 HelloWorld 实例如下:\nusing System; namespace HelloWorldApplication { class App { static void Main(string[] args) { Console.WriteLine(\u0026#34;Hello,World!\u0026#34;); Console.ReadKey(); } } } 程序的第一行 using System; - using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句。 下一行是 namespace 声明。一个 namespace 里包含了一系列的类。HelloWorldApplication 命名空间包含了类 HelloWorld。 下一行是 class 声明。类 HelloWorld 包含了程序使用的数据和方法声明。类一般包含多个方法。方法定义了类的行为。在这里，HelloWorld 类只有一个 Main 方法。 下一行定义了 Main 方法，是所有 C# 程序的 入口点。Main 方法说明当执行时 类将做什么动作。 下一行 /\u0026hellip;/ 将会被编译器忽略，且它会在程序中添加额外的 注释。 Main 方法通过语句 Console.WriteLine(\u0026ldquo;Hello World\u0026rdquo;); 指定了它的行为。 最后一行 Console.ReadKey(); 是针对 VS.NET 用户的。这使得程序会等待一个按键的动作，防止程序从 Visual Studio .NET 启动时屏幕会快速运行并关闭。 WriteLine是一个定义在 _System 命名空间中的 Console 类的一个方法。该语句会在屏幕上显示消息 \u0026ldquo;Hello World\u0026rdquo;。\n以下几点值得注意：\nC# 是大小写敏感。 所有的语句和表达式必须以分号（;）结尾。 程序的执行从 Main 方法开始。 与 Java 不同的是，文件名可以不同于类的名称。 编译运行 # 控制台移动到项目目录.输入 csc Program.cs 进行编译.\n输出如下内容说明编译成功也说明你的 CSC 环境是没问题的:\nMicrosoft (R) Visual C# Compiler version 4.8.4084.0 for C# 5 Copyright (C) Microsoft Corporation. All rights reserved. This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240 如果碰到无法识别 CSC 命令,需要手动天际环境变量:\n在 Path 字段下 添加 C:\\Windows\\Microsoft.NET\\Framework\\v2.0.50727\\\n编译完成后会在项目目录下发现生成一个 Program.exe 的可执行文件,\n双击打开会输出 Hello,world!\n基本语法 # 示例\nusing System; namespace Application { class Rectangle { // 成员变量 double length; double width; // 成员函数 public void Acceptdetails() { length = 4.5; width = 3.5; } public double GetArea() { return length * width; } public void Display() { /* * 这里是多行注释 */ Console.WriteLine(\u0026#34;Length: {0}\u0026#34;, length); Console.WriteLine(\u0026#34;Width: {0}\u0026#34;, width); Console.WriteLine(\u0026#34;Area: {0}\u0026#34;, GetArea()); } } class ExecuteRectangle { static void Main(string[] args) { // 创建一个 Rectangle 实例 (单行注释) Rectangle r = new Rectangle(); // 调用实例中的方法 r.Acceptdetails(); r.Display(); Console.ReadKey(); } } } 执行结果:\nLength: 4.5 Width: 3.5 Area: 15.75 using 的用法 # 以 using 关键字开头的一行代码（例如using System;）可以称为一条 using 语句，几乎所有的 C# 程序都是以 using 语句开头的。using 语句主要用来引入程序中的命名空间，而且一个程序中可以包含多个 using 语句。\nclass 关键字 # class 关键字用来声明一个类，后面是类的名字，class 关键字与类名之间使用一个空格分隔。\n注释 # 注释用于对代码进行解释说明，在编译 C# 程序时编译器会忽略注释的内容。C# 中有单行注释和多行注释两种：\n多行注释 # 多行注释以/*开头，并以*/结尾，/*和*/之间的所有内容都属于注释内容，如下所示：\n/* C# 多行注释 C# 多行注释*/ 单行注释 # 单行注释由//符号开头，需要注意的是单行注释没有结束符，而且只对其所在的行有效，//符号之后的所有内容都属于注释内容，如下所示：\n// 单行注释 成员变量 # 成员变量是用来存储类中要使用的数据或属性的。在上面的示例程序中，Rectangle 类中包含两个成员变量，分别是 length 和 width。\n成员函数 # 成员函数（也可以称为成员方法）是执行特定任务的语句集，一个类的成员函数需要在类中声明。上面的示例代码中，Rectangle 类包含三个成员函数，分别是 AcceptDetails、GetArea 和 Display。\n类的实例化 # 通过一个已有的类（class）创建出这个类的对象（object）的过程叫做类的实例化。类的实例化需要使用 new 关键字，例如上面示例中第 26 行就创建了一个 Rectangle 类的对象。\n标识符 # 标识符是用来为类、变量、函数或任何其他自定义内容命名。C# 中标识符的定义规则如下所示：\n标识符必须以英文字母A-Z、a-z开头，后面可以跟英文字母A-Z、a-z、数字0-9或下划线_； 标识符中的第一个字符不能是数字； 标识符中不能包含空格或特殊符号，例如? - + ! @ # % ^ \u0026amp; * ( ) [ ] { } . ; : \u0026quot; ' / \\，但是可以使用下划线_； 标识符不能是 C# 关键字。 数据类型 # 在 C# 中，变量分为以下几种类型：\n值类型（Value types） 引用类型（Reference types） 指针类型（Pointer types） 值类型 (Value types） # 值类型变量可以直接分配给一个值。它们是从类 System.ValueType 中派生的。\n值类型直接包含数据。比如 int、char、float，它们分别存储数字、字符、浮点数。当声明一个 int 类型时，系统分配内存来存储值。\nusing System; namespace Application { class App { static void Main(string[] args) { int A = 10; bool B, C; var D = true; B = true; C = false; Console.WriteLine(A); Console.WriteLine(B); Console.WriteLine(C); Console.WriteLine(D); } } } 10 True False True 也可以使用 var 关键字来隐式声明变量,但是在 C# 中var 声明要注意以下几点:\n必须在定义时初始化。也就是必须是 var s = “abcd”形式，而不能是如下形式: var s; s = “abcd”;\n一但初始化完成，就不能再给变量赋与初始化值类型不同的值了。\nvar 要求是局部变量。\n使用 var 定义变量和 object 不同，它在效率上和使用强类型方式定义变量完全一样。\n引用类型（Reference types） # 引用类型不包含存储在变量中的实际数据，但它们包含对变量的引用。\n换句话说，它们指的是一个内存位置。使用多个变量时，引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的，其他变量会自动反映这种值的变化。内置的 引用类型有：object、dynamic 和 string。\n对象（Object）类型 # 对象（Object）类型 是 C# 通用类型系统（Common Type System - CTS）中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象（Object）类型可以被分配任何其他类型（值类型、引用类型、预定义类型或用户自定义类型）的值。但是，在分配值之前，需要先进行类型转换。\n当一个值类型转换为对象类型时，则被称为 装箱；另一方面，当一个对象类型转换为值类型时，则被称为 拆箱。\nusing System; namespace Application { class App { static void Main(string[] args) { Object obj = 1000; } } } 动态（Dynamic）类型 # 您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。\n声明动态类型的语法：\n``\ndynamic \u0026lt;variable_name\u0026gt; = value; using System; namespace Application { class App { static void Main(string[] args) { dynamic d = 1000; Console.WriteLine(d); } } } 动态类型与对象类型相似，但是对象类型变量的类型检查是在编译时发生的，而动态类型变量的类型检查是在运行时发生的。\n字符串（String）类型 # 字符串（String）类型 允许您给变量分配任何字符串值。\n字符串（String）类型是 System.String 类的别名。它是从对象（Object）类型派生的。字符串（String）类型的值可以通过两种形式进行分配：引号和 @引号。\nusing System; namespace Application { class App { static void Main(string[] args) { string s = \u0026#34;String!\u0026#34;; string str = @\u0026#34;String\u0026#34;; Console.WriteLine(s); Console.WriteLine(str); } } } 而@ 字符串和引号字符串不同的是: @ 字符串中可以任意换行，换行符及缩进空格都计算在字符串长度之内。 比如:\nusing System; namespace Application { class App { static void Main(string[] args) { string s = \u0026#34;String!\u0026#34;; string str = @\u0026#34;QWERTYQWERTY QWERTY QWERTY QWERTY QWERTY \u0026#34;; Console.WriteLine(s); Console.WriteLine(str); } } } 输出结果:\nString! QWERTYQWERTY QWERTY QWERTY QWERTY QWERTY 指针类型（Pointer types） # 针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。\n语法:\ntype* identifier; using System; namespace Application { class App { static void Main(String[] args) { unsafe { // 声明一个int变量 a int a = 100; // 获取变量a指向的地址值 int* ptr = \u0026amp;a; // 控制台输出 Console.WriteLine((int)ptr); } } } } 直接执行编译会报如下错误:\nProgram.cs(6, 13): [CS0227] 不安全代码只会在使用 /unsafe 编译的情况下出现 (Unsafe code may only appear if compiling with /unsafe) 出现原因:\n在写任意一个 C#程序的时候，一般都是在创建托管代码。托管代码是在 Common Language Runtime (CLR)控制下执行，CLR 使得程序员不需要管理内存和关心内存的分配和回收，有自动回收内存的机制。CLR 也允许你写非安全代码 (unsafe code)。\n非安全代码就是不在 CLR 完全控制下执行的代码，它有可能会导致一些问题，因此他们必须用 \u0026ldquo;unsafe\u0026rdquo; 进行表明，所以一般如果在 C#中到用 unsafe code 非安全代码的话，VS.NET 中编译就会出现“Unsafe code may only appear if compiling with /unsafe”的提示\n如果是 Rider 的话,双击报错,会发现使用过指针的代码旁边会显示一个红色的灯泡.\n鼠标点击那个灯泡然后点击 Allow unsafe code in this project 就可以允许不安全代码了\n变量 # 一个变量只不过是一个供程序操作的存储区的名字。\n在 C# 中，每个变量都有一个特定的类型，类型决定了变量的内存大小和布局。范围内的值可以存储在内存中，可以对变量进行一系列操作。\n类型 示例 整数类型 sbyte、byte、short、ushort、int、uint、long、ulong 和 char 浮点型 float 和 double 十进制类型 decimal 布尔类型 true 或 false 值，指定的值 空类型 可为空值的数据类型 定义 # C# 中变量定义:\n\u0026lt;data_type\u0026gt; \u0026lt;variable_list\u0026gt;; data_type: 数据类型 variable_list: 变量数量\nint i, j, k; char c, ch; float f, salary; double d; 当然也可以在定义的时候进行初始化.\nint A = 100; 变量初始化 # 变量通过在等号后跟一个常量表达式进行初始化（赋值）。初始化的一般形式为：\nvariable_name = value; int x = 1; bool y = false; double z = 0.1; using System; namespace Application { class App { static void Main(String[] args) { int x = 1; bool y = false; double z = 0.1; Console.WriteLine(x); Console.WriteLine(y); Console.WriteLine(z); } } } 输出:\n1 False 0.1 接受来自用户的值 # System 命名空间中的 Console 类提供了一个函数 ReadLine()，用于接收来自用户的输入，并把它存储到一个变量中。\nusing System; namespace Application { class App { static void Main(String[] args) { int Age; Console.WriteLine(\u0026#34;欢迎使用年龄计算器! :-)\u0026#34;); Console.Write(\u0026#34;请输入年龄: \u0026#34;); Age = Convert.ToInt32(Console.ReadLine()); Console.WriteLine(\u0026#34;计算得出:\u0026#34;); Console.WriteLine(\u0026#34;您的年龄是: {0} 岁!\u0026#34;,Age); Console.WriteLine(\u0026#34;(按下回车结束)\u0026#34;); Console.ReadKey(); } } } 函数 Convert.ToInt32() 把用户输入的数据转换为 int 数据类型，因为 Console.ReadLine() 只接受字符串格式的数据。\n","date":"2023 年 03 月 07 日","externalUrl":null,"permalink":"/posts/c-sharp-%E5%9F%BA%E7%A1%80/","section":"Posts","summary":"","title":"C# 基础","type":"posts"},{"content":" 在我的名片上写着，我是一个公司的总裁。\n在我自己来看，我是一个游戏开发者。\n在内心深处，我实际上是一个玩家。\n今天，我要在这里介绍我们的工作和产业。\n我记得，我玩的第一个游戏是《Pong》。我非常喜欢这款游戏。那时候，上高中的我是班上第一个买得起Hewlitt Packard小型计算器的学生。我想这是我产生对游戏兴趣的原因之一。当班上绝大多数同学使用计算器解答数学题的时候，我却用这个小东西编制游戏。我的第一个作品是一款网球游戏。我不期待现在会有什么人说“这款游戏图形效果实在太糟糕”，因为它本身就没有图形效果，而游戏也仅仅是一些数字所组成的。但是，我的朋友们喜欢这款游戏。这使得我非常骄傲。这激发了我（对游戏）的活力和热情。这种对游戏的热情最后确定了我的人生之路。\n1978年，我进入东京工业大学（Tokyo Institute of Technology）学习。在那里，我非常想学习一些视频游戏编程方面的知识，但是那时候没有老师教授这方面的课程。所以，我选修了一些工程以及早期电脑方面的知识。下课之后，我没有和其他同学一起回宿舍继续学习，而是骑上我的小摩托车来到东京的一家电子商店。这家商店是当时东京唯一的一家专门销售个人电脑的店铺。这里是我非常喜欢逛的地方。但是，我不是这家店唯一的“常客”。一些电脑爱好者也经常光顾这家店。并且，我清楚他们在和我考虑一模一样的事——“我如何使用这些个人电脑来玩游戏？”\n一来二去，我和这些电脑爱好者成为了朋友。不久之后，我们组织了一个俱乐部，并且在东京的秋叶原（Akihabara）租了一个单独的公寓以开发自己的游戏。后来，我们的这个开发小组，就成为了今天的HAL公司。公司的名字——“HAL”，取自电影《2001太空漫游（2001: Space Odyssey）》中出现的电脑。当时，在我们来看，这个名字绝对非常响亮而且帅气。这就是我当时的样子。\n[岩田聪先生展示了自己年轻时的一张照片。在照片中，年青的他坐在一辆小摩托车上。]\n和所有的游戏制作人一样，那时的我也是非常“酷”的。\n现在想起来，我都不知道那时我是如何完成了大学学业并且毕业的。等到就业问题出现的时候，我因为已经加入了一个有史以来“最小”的公司而“声名显赫”。目前，我是留下的5名HAL创始员工之一。但是，当时我父母对于我“加入游戏公司”这件事持反对态度。你可以想象，当我把这个决定告诉父亲的时候，他有多么的生气。那段时光，是我家历史上最不愉快的一个时期。\n当有人问我“当你进入HAL时，你负责什么工作”的时候，我会告诉他我是作为一名程序员进入公司的。并且，我同时还兼任开发工程师、游戏设计师以及销售人员。此外，我还负责为大伙订饭以及公司的清扫。虽然工作非常多，但是在我来看，这些都充满了乐趣。\n当听到任天堂在开发一款叫做“Famicom”或者“NES”的新设备、并且这种机器将提供令人难以置信游戏图像的时候，我们意识到HAL历史上最重要的时刻就要到了。我们清楚的知道，这款机器是为我们开发的。于是，我们想尽办法和任天堂公司联系，把我们的创意提供给他们。我们期待其中一个能够获得任天堂的喜爱而成为产品。\n最后，任天堂公司接受了我们。我们成为了Nintendo的员工。在这里，我们的第一个工作并不是把自己的创意付诸实施，而是为任天堂制作一款超期开发的游戏。最后，我们完成了这个工作。而这款游戏就是后来出现在任天堂“红白机（NES/Famicom）”上的《Pinball》。这次开发体验让我们了解到即便是游戏的美工也需要了解游戏开发中商业部分的情况。毕竟如果一款游戏不能实现商业运作，打算通过其挣钱的概率就非常之小。\n从另一个角度来说，那时的工作对我们也是有好处的。因为现在来看，那时的图象是非常粗糙的。因此，我们时常思索如何让玩家的想象超出屏幕的限制。比如我们常常这样想：如果有一天，游戏的图象效果不能进一步提升，我们应该做些什么？\n因为我们的工作获得了任天堂的赏识，此后HAL便和Nintendo建立了非常紧密地关系。在HAL开发的一些早期游戏过程中，我们学到了很多新的东西。第一个“卡比（Kirby）”游戏的开发，使我们懂得了团队协作的重要性。既然不是每个人都能成为宫本茂（Shigeru Miyamoto），团队协作就能开发出一个人能力所及之外的游戏。当时，我们和日本著名制作人系井重里（Shigesato Itoi）一起工作。实际上，他是一个非常痴迷游戏的玩家。此后，他把自己的梦想变成了现实，这就是《Mother(母亲)》系列游戏（北美市场的名称为《Earthbound》）。这件事告诉我们，在合适的情况下不断的追求也能成为[游戏开发的]“原动力”。\n在HAL工作了一些年之后，我进入了Nintendo公司。3年前的一天，山内溥（Hiroshi Yamauchi）老先生指定我担任任天堂的新董事长。这确实是一个所有人期盼的荣誉。但在我而言，这也同时是一个非常大的挑战。我明白这需要我为公司付出更多的时间和精力，同时还要担负更多的责任。\n但是，作为游戏开发者的我，对这一切都了如指掌。\n所以，作为已经一名在视频游戏领域工作超过20年的人，我今天早上非常愿意来到这里回答里两个大家经常提到的问题。\n第一个问题是“作为一个拥有20多年工作阅历的开发者，这些年来什么发生了变化？”\n第二个问题是“[这些年来]什么没有发生变化？”\n我想告诉大家，第一种“没有也将不会发生变化的”就是我们娱乐的天性。与其他媒体一样，我们游戏业为了获得成功同样需要创造一种“情感响应”。笑声、恐惧、愉悦、感动、惊讶等等等等。最后，[我们的产品]能否引起玩家的“共鸣”将作为判断产品成功与否的标准。与此同时，这也是“度量”成功的最低线。\n第二种不变的，就是我们需要对游戏中设定的挑战以及回报[对玩家产生的影响]进行权衡。不同的玩家能够接受的程度各异。一个“重度”玩家可以接受更多的挑战，但是一个普通玩家能够承受的则相对较少。我们认为开发出适合所有玩家的游戏是任天堂的责任。当然，我在这里所提到的“所有玩家”也包括现在还不曾玩过我们游戏的朋友。\n第三种不会改变的就是创意的“重要性”。当然，这也包括为已有的游戏提供一个新的开发思路。但是，最难能可贵的就是能够提出一个全新的游戏创意。我确信，在今天的台下的听众中就有这样的天才。我们游戏产业需要的就是这种天才！！\n第四种，也是永远不会改变的，就是“依靠软件贩卖硬件”的销售模式。人们买游戏是为了玩到他们喜欢的游戏。在这一点上，我同意苹果（Apple）公司总裁史蒂夫•乔布斯（Steve Jobs）关于“软件就是使用者的体验”、“软件不单单将推动电脑技术的发展，它将推动整个消费电子产业的发展”的看法。\n最后，不会改变的就是知识产权的价值。如果“依靠软件贩卖硬件”的销售模式是正确的话，那么“知识产权贩售软件”的正确性将更加彰然。当开发商用“超人”、“詹姆斯•邦德”以及“NFL Football”这些品牌创造商业奇迹的时候，我们应该为能够使用自己创造的这些品牌来制作最好的游戏而骄傲。\n再来说说，我是如何看待“什么发生了变化”的。\n[在我思考这个问题时]一个字眼立刻“跳到”我的脑海之中，那就是“更大”。\n在西半球，游戏产业变得“更大”。在北美，仅游戏零售店的价值就已经接近170亿美元。去年，美国游戏销售再次获得了8%增长率。现在游戏无处不在。它在你的起居室里、你的办公室里、你的PDA里、你的手机里，当然还有你的NDS里。许多媒体的报道都显示，现在的年轻人用在玩游戏上的时间远远超过了看电视。\n与此同时，游戏本身也在不同的方向上变得“更大”。依次是，更大的开发团队、更大的开发预算以及面临发售期限时更大的挑战。此外，“更大”也意味着原先大型的开发公司，通过毁灭相对较小的同行，而变得“更大”。我们都非常清楚，下一代游戏的开发预算将至少是现在的3倍以上。随便一款游戏的开发成本将达到8位数。如此巨额的预算也只有那些“更大”的开发商才能承受得了。\n开发商在重点游戏上“更大”的投入将会为他们带来更多的利润。这在所有人来看都是非常正常的。但是，不知道大家注意到没有，开发商提供给每个消费者的书、电影以及电视节目都是千篇一律的。为此，我们[任天堂]将做出自己“不落窠臼”的风格。在游戏中，我们将帮助玩家完成自己的梦想、创造自己的精彩结局。\n另一方面，更明显的是产业的“缩小”趋势。\n现在，我们所能遇到的风险越来越小。我们越来越容易对“什么是视频游戏”作出定义。游戏的类型也非常清楚：射击类、体育类、益智类等等。不过，谁还记得我们上一次创造出新的游戏类型是什么时候？而且，更重要的是我们用来诠释游戏的方式也越来越少。“赛道”、“音轨”、“老怪（Bosses）”还有英雄。不同的游戏制作的越来越相似。“泰戈•伍兹（Tiger Woods）”和“马里奥高尔夫（Mario Golf）”，都是非常成功的游戏大作。但是两款同样以“高尔夫”为主题的游戏，却属于截然不同的两种游戏类型。这种多样性在现今越来越少见了。\n现在，我们也越来越容易定义游戏的开发取向。增加游戏的“真实性”，并不是提高游戏性的唯一办法。在这一点上，我曾经做出冒被大家误解的风险的举动。还记得么，我曾经开发过一款没有网球选手的网球游戏。如果有人非常欣赏图形效果，最应该的就是我了。但是，我的意思是图形效果能够对游戏有所改善。我们需要找到其他的方法。游戏性的“改进”应该有更多的定义。\n最后，我非常关心玩家的想法。\n当我们花费更多的时间和金钱来满足玩家的时候，我们是否遗漏了其他一些玩家？ 我们开发的游戏是否适合每一个人？ 是否你有朋友和家人不玩游戏？那他们喜欢做什么？ 我会提问[在座的所有人]：是否你曾经为了一款自己都不愿意玩的游戏而挑战自我、辛苦开发呢？\n我想这些问题对于在所得所有人来说都是一个非常重要的挑战。\n所以，我已经多次对游戏产业当前的状况发表看法。你可能对于我们将如何解决上述问题感到好奇。在这里，我将坦诚地做出回答。\n首先，[第一个问题是]是否任天堂背离了“重度”玩家？我不这样认为。\n如果我们不在乎“重度”玩家的话，我们就不会为NDS准备《银河战士：猎手（Metroid Prime Hunters）》。虽然，这款游戏并不能称得上是一款“经典大作”，但是它表明我们的NDS用户定位中并不缺失“重度”玩家。而且，如果我们不在乎这个玩家群落的话，我们也不会和n-Space合作推出专门针对NGC主机的《Geist(感性)》。这些都将改变您的看法。\n[再举个例子，]如果我们不重视“重度”玩家，NGC就不会成为2005北美首款成功大作——Capcom的《生化危机4（Resident Evil 4）》的首发游戏平台。这不但证明我们非常关心“重度”玩家，而且表明这个层面的玩家也同样关心任天堂。\n而且，我们将按部就班地推出今年任天堂游戏平台最受期待的游戏——一款全新的“塞尔达传说”。我非常高兴能和在座各位谈论这款游戏，但是视频更具说服力。任天堂公司选择您在E3大展之前欣赏这款最新“塞尔达传说”的动画。\n大家在这里了解到的仅仅是这款全新“塞尔达传说”的一小部分。我们将在今年的E3大展上公布更多细节。最新“塞尔达传说”将面向核心玩家。同时，这款作品也会和前作一样适合所有玩家。这也是我们[任天堂]开发所有游戏的标准。\n我们的游戏开发标准可以形象地用4个“I”表示：\n第一个“I”——真正的创新（Innovative）。[在我们的游戏中具有]此前所没有的东西。\n第二个“I”——直觉（Intuitive）。[在游戏开发的时候]如何自然地控制角色，如何定位游戏性，都来自于直觉。\n第三个“I”——游戏的魅力（Inviting）。什么将会使你肯为游戏花费时间呢？那就是游戏的魅力。\n最后一个“I”——界面（Interface）。什么样的界面才能吸引玩家呢？玩家能否以一种全新的形式开始游戏呢？\n实际上，在游戏业界几乎没有几款游戏能够同时在这四方面做好。但是，这确实是我们要求自己的标准。\n在实际工作中，我们切实地把这个标准用于我们的软硬件开发。最近的一个例子就是我们发售的NDS主机。这款主机就兼顾了“创新”、“新颖的界面”、“直觉”和“魅力”。到目前为止，玩家们都认可了这一点。现在，我们已经在日本以及北美地区售出了400万台NDS。其中，北美地区从发售到现在仅仅经过了16周。而这些数字还不包括将在明天（本文发表于3月10日）启动的欧洲市场。\n我相信，大家已经非常熟悉这款配备2个屏幕、触摸屏以及麦克风的掌机。但是，这款掌机还有一个非常重要的功能可能被大家忽略了。这就是“无线游戏功能”。目前，我们已经完成了一款支持这种功能的游戏——《马里奥赛车（Mario Kart）》。这款游戏将支持8人对战。对于其他版本的“马里奥赛车”，大家可能都耳熟能详。但是这款NDS版本将会有什么不同呢？好的，让我们来了解一下。\n首先，我需要问在座的大家一个问题。今天这里又没有朋友过生日？如果有的话，请您站起来。\n好的，请到台上来。\n实际上，如果您的生日在本周的任何一天，请起立。因为我需要另外6位朋友来帮助我完成这个展示。\n来吧，不要害羞。\n[听众走向讲台]\n任天堂美国公司的比尔•崔恩（Bill Trinen）也将加入进来，和我们组成一个《马里奥赛车》NDS版无线游戏测试小组。\n从我们头顶上的屏幕，您将看到我的赛车。当然希望是领先的…以及其他朋友的赛车。\n[实时显示由岩田聪、比尔•崔恩以及其他六位听众组成的小组，进行《马里奥赛车》无线对战的情景。]\n这些天，我一直花费很长时间出席会议、接受采访和旅行。有些时候，我都忘记了原来玩游戏时的那些欢乐。好的，这次《马里奥赛车》无线对战的现场展示就到这里。这款游戏产品，我们打算在今年年底把它推向市场。\n下面，我想用剩下的时间来解答下一个问题，那就是“任天堂将向哪里发展”。\n让我尝试用一张图来解答这个问题。\n在互动娱乐产业的“宇宙”之中，有一颗行星叫做“视频游戏”。这一点大家都很清楚。但是，它是唯一的一颗[行星]。如同我们所在的宇宙中存在许多其他的行星一样，在“游戏产业”这片小星空中，还有很多不同类型的“游戏”行星亟待人们去探索。\n基于这一观点，任天堂形成了自己的两个努力方向。\n一方面，我们辛勤工作要把“视频游戏”做得更好。我们要让玩家们得到他们梦想中的游戏。与此同时，我们还在专心寻找这个领域中能为我们所用的其他资源。\n我们的另一个方向就是为玩家提供一些他们不曾想象得到的新东西。得到很多玩家喜爱的“Pokémon”，就是基于这个理念而开发出来的。在游戏来说，“Pokémon”是非常好的RPG游戏。但是，它的意义不仅限于此。在游戏中，玩家可以收集和交换“Pokémon”。而这可能和你小时候收集交换瓶盖和棒球卡的方式一样。\n所以，“Pokémon”把RPG游戏扩展到了此前未曾涉及的领域。\n另一个例子就是我们为NDS开发的“PictoChat”。这个设定不是某种游戏，或者某种挑战。它是一种让我们体会如何进行无线交流的途径。作为无线应用的“PictoChat”，将成为任天堂迈向更广阔领域的具有标志性意义的一步。\n今天，我在这里宣布，基于已经完成的网络架设，我们将从NDS主机开始，战略性地为玩家提供Wi-Fi接入服务。\n在任天堂的历史上：\n最初，2部Game Boy可以通过线缆进行连接。 此后，4部Game Boy Advanced通过线缆相互连接。 后来，我们为游戏主机提供了4个手柄，并且最终实现了无线控制。 再后来，我们为《Pokémon：Firered（口袋妖怪：火红）》和《Pokémon：Leafgreen（口袋妖怪：绿叶）》两款游戏附上了无线适配器。\n[在这里，我想通过这段叙述，为那些还不了解这一进程的朋友介绍我们的一次次进展。]\n现在，该轮到NDS了。\n每一部NDS都是按照“能够获得每一个玩家喜爱”的原则进行设计制造的。因此，[我想]玩家们也一定会非常容易接受Wi-Fi服务。我们的目标就是将这个“过程”尽可能简单化。以至于，玩家接受它时根本不需要考虑什么。因为使用了普通的应用程序接口（API）,Wi-Fi接入服务将会和本地互联网连接一样方便快捷。与此同时，我们提供的服务也将不需要用户输入SS-ID或者WEB密码。\n但是，我们提供的这种服务最重要的就是将“完全免费”。这将解决用户使用Wi-Fi服务的最大障碍——资费。\n现在您可能想知道这个服务的基础架构是否完工。我可以和您说，这项工作“几乎差不多[完成]了”。[那您也许会问]如何开发[基于此项服务的应用]呢？从哪里可以得到开发工具？在E3大展上，我们将提供更多的资料给您。\n好的，[我们谈谈]这种服务将带来什么样的娱乐内容?\n我现在可以告诉大家，年内大家就可以在NDS平台上玩到Wi-Fi游戏。我可以自豪地说，这种游戏非常有趣。至少，这些游戏中的一部分已经开始调试。让我们一起期待Wi-Fi游戏的到来。\n让我为大家举一个正在开发之中的游戏的例子。正在开发的《动物之森（Animal Crossing）》Wi-Fi。我之所以选择这款游戏，是因为它具有以下一些特点。\n首先，这款《动物之森》是众多“非游戏游戏（non-game games）”之一。我的意思是，娱乐形式并不一定就需要“获胜”或者“得出某个结论”。由于这款游戏的使用范围是无限的，它也就无形中解决了无线通信的延迟问题。此前，你可以在不同的城市间玩这个游戏；而现在有了Wi-Fi，你可以在世界的任何角落都能继续你的存档。于是，我们相信自由和便捷的无线游戏，将把[任天堂的]游戏产业带到一个新的方向上。\n在这里，我要再次请出比尔•崔恩为我们展示两款交互性娱乐世界的新创造。在展示之前，我还是要对各位说，它们可能看起来非常不寻常。但正是它们的与众不同，才使得如此“不寻常”。\n第一个游戏来自于宫本茂先生的创造。\n请比尔•崔恩演示。\n[比尔•崔恩启动了《任天狗（Nintendogs）》的演示动画]\n正如大家所看到的，这款游戏将把那些曾经不满意任天堂游戏的朋友变成我们忠实的玩家。下面，我将让比尔•崔恩为我们展示另一款游戏。我们把它叫做《Electroplankton（电子浮游生物）》。这个游戏的名字听起来很奇怪，不是么？电子—浮游—生物。它玩起来也同样与众不同。[当你看完这段演示之后，就会明白]创造力并不仅属于游戏开发者。它同样属于玩家。\n[比尔•崔恩启动了《Electroplankton（电子浮游生物）》的演示动画]\n这款游戏非常特别。它的主旨是实现协调，而不是为了制造人们****上腺素。到现在为止，我们已经了解到玩家对于这款游戏的不同反应。一些玩家可能感到困惑，因为他们找不到“得分”或者“下一个敌人”。但是，另一些玩家却对这款游戏非常着迷。这些人甚至拒绝中止游戏。不管你的对这款游戏的看法如何，我想你一定赞同以下的看法，即“这款游戏[的创意]并非来自于我们所熟知的‘视频游戏’”。\n谢谢比尔。\n任天堂的计划就是把现在的游戏世界变得更好。[我们将带给大家]更好的“塞尔达传说”、更好的“马里奥”以及更好的“生化危机”。\n但与此同时，我们也在不断探索互动娱乐中的“新世界”。\n对我们而言，这是一次冒险之旅。但更重要的是，我们需要您——带着游戏产业所需要的创造力——和我们共赴征程。\n我想大家还记得，我们在去年E3大展上是如何解释“NDS”名字中的“DS”的。我说这代表两个意思：双显示屏幕（Dual Screen）以及[属于]开发者的系统（Developer’s System）。[下面我将为大家介绍的]任天堂的[次时代主机]“Revolution（革命）”也将是一款[属于]开发者的系统。\n在IBM公司的帮助下，我们为“Revolution”准备了中央处理器。我们把它命名为“Broadway（百老汇）”。在我们来看，“百老汇”代表的是整个娱乐产业。在ATI公司的协助下，我们为新主机准备了图形处理器。它的名字叫做“Hollywood（好莱坞）”。[我想大家能够看得出来，]这些名字寄托着我们对这款主机的期望。\n现在，我为大家提供一些这款主机的资料。\n首先，我在这里正式宣布“Revolution”将具有向下兼容能力。这样，拥有NGC的玩家将可以在新主机上继续享受自己喜爱的游戏了。其次，一如我此前说过的，这款新主机将支持无线网络技术。我此前提到的Wi-Fi功能，将延伸到“Revolution”上。第三，虽然这款游戏主机将为玩家提供与众不同的游戏体验，但是游戏的开发仍旧采用大家所熟悉的方式，而不会有太大的变化。\n在这方面，新主机与NDS相同。他们都是以“创意”而不是“预算”取胜的。\n但是，大家不要误解我的话。我们[任天堂]非常热切地期盼获得第三方游戏开发商的鼎力支持。基于上述观点，任天堂将扩充其开发力量，以实现对所有产品线的支持。此后，一些规模更大的团队将出现在我们产品开发部门。而且，我们也将把一些游戏的研发工作交给和任天堂拥有多年合作关系的第三方厂商。\n如果您不介意的话，我想以回顾开发过的游戏的方式来结束今天的演讲。\n在我的工作中，《Super Smash Bros（任天堂全明星大乱斗）》的开发过程曾经给我留下深刻印象。虽然这款NGC游戏开始开发的时候，我已经转入任天堂公司。但我的心告诉我，“你仍然是一名游戏开发者”。于是，我以公司总裁的身份，把自己重新分配到HAL公司以完成这款游戏。这样，我重新体验了游戏开发乐趣——开发讨论会、比萨饼、饭团和通宵达旦的工作。在开发团队的办公室里，有时候你能够看到富士山（Mount Fuji）。尤其是朝霞中的富士山，显得格外高大雄伟。光阴似箭，如此的胜景已经陪伴着我们和HAL公司度过了许许多多个岁月。经常通宵工作的我们，对在上床休息之前能够欣赏如此美景已经习惯了。\n很多人都说富士山的第一缕光芒在鼓舞着我们[不断前进]。 但是对于我来说，我希望永远再也不要看到这个情景了。\n同样，我也非常怀念为N64主机开发的那款“任天堂全明星”游戏。那款游戏的人物设定使用的是任天堂的经典游戏角色，后来它所产生的效果是“震撼”的。但是，当时这个主意并不是新颖。因为此前已经有一些类似的游戏出现了。因此，当我们把这个想法提供给任天堂时，公司内外的很多人都不看好它。但是，这次我们将重点放到了游戏环境的营造上。直到我们的测试员表示出对这款游戏的极大兴趣，公司此前所持的观点才有所改变。\n后来，依次发生了下面这些事情： [测试游戏的]员工开始微笑。 员工开始大笑。 员工高兴地开始相互喊叫。\n自此，公司对这款游戏的看法完全改变了。这次成功，是我工作这么长时间以来最骄傲的一次。后来，这款游戏在世界范围内获得了完全的成功。到目前为止，它已经销售出了1000万份。但是，现在，最初测试人员的情绪变化还时不时地出现在我的脑海中。\n我把这叫做“成功”。\n在HAL工作中，我们找到了把梦想变成现实的方法。我们的团队将坚持自己的想法。我们不会在前进的道路上退缩。出于这个心态，我们所有的HAL员工非常喜欢在座的每一个人。\n即便我们来自世界的不同地方\n即便我们说着不同的语言\n即便我们吃着不同的食品或者饭团\n即便我们在游戏中有不同的体验\n但今天我们在座的每个人有一个非常重要的相同点。\n这个相同点就是我们都拥有同样的“玩者之心”。\n谢谢大家。\n","date":"2023 年 02 月 19 日","externalUrl":null,"permalink":"/posts/%E7%8E%A9%E8%80%85%E4%B9%8B%E5%BF%83/","section":"Posts","summary":"","title":"GDC2005任天堂岩田聪演讲全文：玩者之心","type":"posts"},{"content":"","date":"2023 年 02 月 19 日","externalUrl":null,"permalink":"/categories/%E6%91%98%E6%8A%84/","section":"Categories","summary":"","title":"摘抄","type":"categories"},{"content":"","date":"2022 年 12 月 29 日","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":" 什么是 Linux # Linux，全称 GNU/Linux，是一种免费使用和自由传播的类 UNIX 操作系统，其内核由林纳斯·本纳第克特·托瓦兹于 1991 年 10 月 5 日首次发布，它主要受到 Minix 和 Unix 思想的启发，是一个基于 POSIX 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议。它支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想，是一个性能稳定的多用户网络操作系统。Linux 有上百种不同的发行版，如基于社区开发的 debian、archlinux，和基于商业开发的 Red Hat Enterprise Linux、SUSE、Oracle Linux 等。\n安装 Linux # 这里使用的环境是 Windows 自带的 Linux 子系统 (WSL2), Ubuntu 版本 1804. 可以参考微软官方的安装教程 安装 WSL | Microsoft Learn 注意的是,如果直接从微软商店(Microsoft Store)安装会默认安装的 C 盘,导致使用不便. 建议下载发行版的离线安装包,然后将后缀改为 zip,解压到你想要的位置上面,接着运行可执行文件(exe),安装 Linux.\nLinux 目录结构 # Linux 的目录结构是一个树型结构. Windows 系统可以拥有多个盘符,如 C 盘、D 盘、E 盘. Linux 没有盘符这个概念，只有一个顶级目录: 根目录( / ), 所有文件都在它下面.\n所以 Linux 的目录描述起来就是 /home/user , 而 Windows 就是 D:\\Ubuntu_1804.2019.522.0_x64\n在 Linux 中 / 在开头表示根目录,在后面的表示一个层级关系, 比如前面的 : /home/user 中, / 表示根目录,home/user 表示 根目录 下 home 文件夹下的 user 文件夹.\n注意 # 在 Linux 中描述路径用的是斜杠 / 来表述路径的,而 Windows 系统中使用的是反斜杠来表示 \\ .\n反斜杠为啥存在,可以参考下知乎 Lunamos 的回答:\n直接看维基就好了.\n当次搬运工吧，Bob Bemer（ASCII 之父，当时的一位 IBM 工程师）在 1961 年将反斜杠（也就是「\\」，正斜杠是「/」）引入 ASCII，原因是在频率分析中认为反斜杠还是被用了很多的一个符号。再仔细追究会发现，反斜杠最主要的用途是在一个叫做 ALGOL 的早期高级语言)中参与表示布尔运算 AND 和 OR，其中 AND 的写法是「/\\」，OR 的写法是「\\/」，都是分别由一个反斜杠和一个正斜杠组成的。而之后流传了下来，并被用作更多其他的用途。\n考证贴的存档：https://web.archive.org/web/20130119163809/http://www.bobbemer.com/BACSLASH.HTM\n原回答地址: 反斜杠为什么存在？ - Lunamos 的回答 - 知乎\n命令, 命令行 # 命令:即 Linux 程序。一个命令就是一个 Linux 的程序。命令没有图形化页面，可以在命令行(终端中)提供字符化的反馈\n命令行:即 Linux 终端(Terminal),是一种命令提示符页面。以纯“字符”的形式操作系统，可以使用各种字符化命令对系统发出操作指令.\nLinux 命令基础格式 # 无论是什么命令，用于什么用途,在 Linux 中，命令有其通用的格式: command [-options] [parameter] 其中 ,\ncommand:命令本身 -options:(可选,非必填)命令的一些选项,可以通过选项控制命令的行为细节 parameter:(可选,非必填)命令的参数，多数用于命令的指向目标等\n语法中的[],表示可选的意思.\n例如:\ncat -E /home/user/t1/t1.txt : 查看 /home/user/t1/t1.txt 的内容,并且没每行结束以$ 结尾. 其中 cat 是命令本身 , -E 是选项, `/home/user/t1/t1.txt 是参数\nLinux 基础命令 # 路径与特殊路径符 # 绝对路径:以根目录为起点，描述路径的一种写法，路径描述以 / 开头\n相对路径:以当前目录为起点，描述路径的一种写法，路径描述无需以 /开头\n例如:\n我现在在 /home/user 路径下,想要创建一个名为 t1 的目录,绝对路径的写法就是\nmkdir /home/user/t1 而相对路径的写法就是\nmkdir t1 特殊路径符 • . 表示当前目录，比如说 cd . 或 cd ./Desktop\n• .. 表示上一级目录，比如说 cd ..如果想要上上一级目录可用 cd ../..\n•～表示用户的 HOME 目录。比如 cd ～ 或 cd ～/Desktop\nls 命令 # ls [-a -l -h] [Linux路径]\n作用 : 以平铺的形式列出指定路径下的文件\n例如 :\n$ ls 输出\nbin dev home lib lost+found mnt root sbin srv t1 usr boot etc init lib64 media proc run snap sys tmp var 这是我当前工作目录下的文件\n需要注意的是 Linux 在启动的时候,每个 Linux 操作用户在 Linux 系统的个人账户目录, 路径为 /home/用户名. 在用户登录的时候,会默认在账户目录下进行操作,即 user 用户登录的时候,默认会在 /home/user这个用户下操作.\n选项使用 # -a 选项 , 表示 all 的意思,会列出所有文件(包括隐藏文件和文件夹) 例如: $ ls 输出:\nlogs t.txt t1 这里输入 ls -a\n$ ls -a 输出:\n. .bash_history .bashrc .config .local .pki .sudo_as_admin_successful .viminfo t.txt .. .bash_logout .cache .landscape .motd_shown .profile .test.swp logs t1 -l 选项 ,表示以列表的形式竖向排列展示内容,并且展示更多信息 $ ls -l drwx------ 2 user user 4096 Dec 30 16:31 logs -rw-r--r-- 1 user user 42 Dec 30 10:57 t.txt drwxr-xr-x 3 root root 4096 Dec 30 11:14 t1 给出了 drwxr-xr-x : 权限 , user user 用户和用户组 , 4096 文件或者文件夹大小和 Dec 30 16:31 创建时间等数据\n-h 表示易于阅读的形式,列出文件的大小,此选项需要和其他选项连用. 例如 :\n$ ls -lh total 12K drwx------ 2 user user 4.0K Dec 30 16:31 logs -rw-r--r-- 1 user user 42 Dec 30 10:57 t.txt drwxr-xr-x 3 root root 4.0K Dec 30 11:14 t1 或者是\n显示当前目录下所有的文件.并且以列表形式展示,同时变为易于阅读的形式\n$ ls -alh total 76K drwxr-xr-x 9 user user 4.0K Dec 30 17:33 . drwxr-xr-x 3 root root 4.0K Dec 30 10:00 .. -rw------- 1 user user 1.5K Dec 31 11:53 .bash_history -rw-r--r-- 1 user user 220 Dec 30 10:00 .bash_logout -rw-r--r-- 1 user user 3.7K Dec 30 10:00 .bashrc drwx------ 3 user user 4.0K Dec 30 16:31 .cache drwx------ 3 user user 4.0K Dec 30 16:31 .config drwxr-xr-x 2 user user 4.0K Dec 30 17:33 .landscape drwxr-xr-x 3 user user 4.0K Dec 30 16:31 .local -rw-r--r-- 1 user user 0 Dec 31 10:03 .motd_shown drwx------ 3 user user 4.0K Dec 30 16:31 .pki -rw-r--r-- 1 user user 807 Dec 30 10:00 .profile -rw-r--r-- 1 user user 0 Dec 30 10:01 .sudo_as_admin_successful -rw------- 1 user user 12K Dec 30 10:27 .test.swp -rw------- 1 user user 7.8K Dec 30 11:07 .viminfo drwx------ 2 user user 4.0K Dec 30 16:31 logs -rw-r--r-- 1 user user 42 Dec 30 10:57 t.txt drwxr-xr-x 3 root root 4.0K Dec 30 11:14 t1 cd/pwd 命令 # cd 命令 # cd [linux路径]\n切换当前所在工作目录\n长度命令无需选项只有参数,如果不写参数,则会切换到用户的 Home 目录.\n例如: 切换到用户目录下的 t1 目录\n$ cd t1 切换回 Home 目录\n$ cd pwd 命令 # pwd\n查看当前工作目录\n无选择无参数,直接输入 pwd 即可\n$ pwd /home/user mkdir 命令 # mkdir [-p] Linux路径\n创建新的目录\n参数选项必填,选项可选,表示自动创建不存在的父目录.\n$ mkdir t2 直接使用 mkdir t3/t32 会报错\nmkdir: cannot create directory ‘t3/t32’: No such file or directory 所以需要加入选项来创建不存在的父目录\n$ mkdir -p t3/t32 文件操作命令 # touch 命令 # touch Linux路径\n创建文件\ntouch 命令无选项,参数必填,表示创建文件的路径(相对,绝对,特殊路径符皆可).\ncat 命令 # cat Linux路径\n查看文件内容(完全显示)\ncat 同样没了选项,只有必填参数\n$ cat t1.txt 123123123 123123123123 1231231231 Print(\u0026#34;hello,World\u0026#34;) more 命令 # more Linux路径\n作用: 查看文件内容,但是 cat 是将内容全部显示出来,而 more 在文件内容过多的情况下支持分页显示(查看过程中使用空格分页,使用 q 退出查看)\ncp 命令 # cp [-r] 参数1 参数2\n复制文件或者文件夹\n-r 选项可选,用于复制文件夹,表示递归 参数 1 : Linux 路径,表示来源的文件或者文件夹 参数 2 : Linux 路径,表示要目标文件夹 mv 命令 # mv 参数1 参数2\nmv 命令可以移动文件或者文件夹\n参数 1 : Linux 路径,表示来源的文件或者文件夹 参数 2 : Linux 路径,表示要目标文件夹, 如果目标不存在,则进行改名,确保目标存在. rm 命令 # rm [-r -f] 参数1 参数2 ....参数N\n删除文件,文件夹\n和 cp 命令一样, -r 选项用于删除文件夹 -f ,表示强制删除(无确认提示,需要注意的是,普通用户不会弹出提示,只有 root 管理员用户会有提示) 参数 1 参数 2 \u0026hellip;.参数 N 表示要删除的文件或文件夹路径,需要使用空格隔开 rm 命令支持通配符 *,用来做模糊匹配. 符号 * 表示通配符,即匹配任意内容(包括空) . 用法为:\ntest* 表示匹配任何开头是 test 的内容 *test 表示匹配任何以 test 结尾的内容 *test* 表示匹配任何包含 test 的内容 管道符 # 管道符: |\n管道符左边命令的结果,作为右边命令的输入\n文件查找命令 # wc 命令 # wc [-c -m -l -w] 文件路径 -c , 统计 bytes 数量 -m , 统计字符数量 -l , 统计行数 -w , 统计单词数量 文件路径 , 被统计的文件,可作为内容输入端口 which 命令 # which 要查找的命令\n可以查看使用的一系列命令的程序文件在哪里 而前面学期的 Linux 命令实际上就是二进制可执行程序\nwhich 可以查看到命令的文件在哪里\nwhich pwd 输出\n/bin/pwd find 命令 # find 起始路径 [-name] \u0026quot;被查找的文件名\u0026quot; 查找指定文件\n# find /home/user -name \u0026#34;11*\u0026#34; 这里结合通配符来实现复杂一些的搜索\n/home/user/11.conf /home/user/11.txt /home/user/11.yaml 常用参数\n-mount, -xdev : 只检查和指定目录在同一个文件系统下的文件，避免列出其它文件系统中的文件\n-amin n : 在过去 n 分钟内被读取过\n-anewer file : 比文件 file 更晚被读取过的文件\n-atime n : 在过去 n 天内被读取过的文件\n-cmin n : 在过去 n 分钟内被修改过\n-cnewer file :比文件 file 更新的文件\n-ctime n : 在过去 n 天内创建的文件\n-mtime n : 在过去 n 天内修改过的文件\n-empty : 空的文件-gid n or -group name : gid 是 n 或是 group 名称是 name\n-ipath p, -path p : 路径名称符合 p 的文件，ipath 会忽略大小写\n-name name, -iname name : 文件名称符合 name 的文件。iname 会忽略大小写\n-size n : 文件大小 是 n 单位，b 代表 512 位元组的区块，c 表示字元数，k 表示 kilo bytes，w 是二个位元组。\n-type c : 文件类型是 c 的文件。\ngrep 命令 # grep [-n] 关键字 文件路径\n使用 grep 命令可以从文件中通过关键字过滤文件行\n-n，可选，表示要在结果中显示匹配行的行号 参数 （关键字），必填，表示过滤的关键字，带有空格或者其他特殊符号，建议使用 \u0026quot; \u0026quot; 将关键字包围起来 参数（文件路径） ，必填，表示要过滤内容的文件路径，可作为内容输入端口 $ cat 11.conf 123123123123123 12312312313212312 123123123131312312 asdfafafasdf fvasvdfvbasffgaer 123423425345236364 $ grep \u0026#34;asdf\u0026#34; 11.conf asdfafafasdf ","date":"2022 年 12 月 29 日","externalUrl":null,"permalink":"/posts/linux-%E5%9F%BA%E7%A1%80/","section":"Posts","summary":"","title":"Linux 基础","type":"posts"},{"content":" 环境配置 # Goctl 环境配置 # Goctl 介绍 # goctl 是 go-zero 微服务框架下的代码生成工具。使用 goctl 可显著提升开发效率，让开发人员将时间重点放在业务开发上。\n其功能有：\napi服务生成 rpc服务生成 model代码生成 模板管理 Goctl安装 # 方法1 Go get安装 # Go 1.15 及之前版本 GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro/go-zero/tools/goctl@latest # Go 1.16 及以后版本 GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest (windows可以下载64位或者32位的发行版,然后放到$GOPATH/bin 目录下,然后将其添加到环境变量中)\n方法2 从 go-zero代码仓库 git@github.com:zeromicro/go-zero.git 拉取一份源码，进入 tools/goctl/目录下编译一下 goctl 文件，然后将其添加到环境变量中。 安装完成后 goctl -v ,如果输出版本信息则代表安装成功，例如：\n$ goctl -v goctl version 1.4.3 windows/amd64 如果显示没找到,请确认是否配置了环境变量\n环境测试 # 值得注意的是在 1.4.3 版本的 Goctl直接使用go run greet.go -f etc/greet-api.yaml会报错,因为httpx 包的 Error 函数发生了变化.\n# greet/internal/handler internal\\handler\\greethandler.go:16:10: undefined: httpx.ErrorCtx internal\\handler\\greethandler.go:23:10: undefined: httpx.ErrorCtx internal\\handler\\greethandler.go:25:10: undefined: httpx.OkJsonCtx 需要找到路径下 greethandler.go 文件然后把 httpx.ErrorCtx(r.Context(), w, err) 和 httpx.OkJsonCtx(r.Context(), w, resp) 改为 httpx.Error(w, err) 和 httpx.OkJson(w, resp)\n然后打开项目目录下的 greet/internal/logic/greetlogic.go ,在写上如下代码:\nfunc (l *GreetLogic) Greet(req *types.Request) (resp *types.Response, err error) { // todo: add your logic here and delete this line return \u0026amp;types.Response{ Message: \u0026#34;Hello go-zero\u0026#34;, }, nil } 启动服务\n$ cd greet $ go run greet.go -f etc/greet-api.yaml 访问测试\ncurl -i -X GET http://localhost:8888/from/you 请求结果\nHTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Traceparent: 00-86cef6c758a3f8456ec7b4ea331dea08-8340dfb14fdec0cf-00 Date: Tue, 13 Dec 2022 07:21:50 GMT Content-Length: 27 {\u0026#34;message\u0026#34;:\u0026#34;Hello go-zero\u0026#34;} 说明环境搭建完成\n未完待续 # ","date":"2022 年 12 月 13 日","externalUrl":null,"permalink":"/posts/go-zero-%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/","section":"Posts","summary":"","title":"Go-Zero 框架入门","type":"posts"},{"content":" Golang派生类型 # (指针类型（Pointer） 数组类型 (Arrayllist) 结构化类型 (Struct) 通道类型 (Channel ) 函数类型(Func) 切片类型 (Slice) 接口类型（Interface） Map 类型 指针 Pointer # 声明指针\nvar Name *Type Name :指针名 Type: 指针类型 注意: 指针需要指向的是一个具体的地址\n声明地址需要在要声明的变量前面加 \u0026amp;\npackage main import \u0026#34;fmt\u0026#34; func main() { var a int = 10 fmt.Println(a) var b *int b = \u0026amp;a //指针b 指向 a 的地址 *b = 100 //将 b 指向的内存地址赋为 100 fmt.Println(a, *b) //输出 a 和 指针b 指向的值 fmt.Println(a == *b) //查看 a 和 *b 指向的值是否相等 fmt.Println(\u0026amp;a == b) //查看 a 的地址和 b的指针地址是否相等 } 输出结果:\n10 100 100 true true 图解: 数组 Arrayllist # 数组的声明 # 数组是一个由固定长度的特定类型元素组成的序列，一个数组可以由零个或多个元素组成。\nvar name [size] type name : 数组名称\nsize : 数组长度\ntype : 数组中数据类型\npackage main import \u0026#34;fmt\u0026#34; func main() { var Arrayllist [5] int //定义长度为 3 ，数据类型为int ，名为 Arrayllist 的数组 for i := 0; i \u0026lt; len(Arrayllist); i++ { //遍历数组 fmt.Println(\u0026#34;Arrayllist[\u0026#34;, i, \u0026#34;]: \u0026#34;, Arrayllist[i]) //输出 } } 输出结果:\n数组初始化 # 静态初始化: 给出初始化值，由系统决定长度 var Arrayllist = [...]int8{1, 2, 3, 4, 5,6,7,8,9,10} //或者 Arrayllist := [...]int8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 动态初始化: 只指定长度，由系统给出初始化值\nvar Arrayllist = [5] int8 {1,2,3,4,5} //或者 Arrayllist := [5] int8 {1,2,3,4,5} 遍历数组 # 可以使用for循环来遍历数组:\npackage main import \u0026#34;fmt\u0026#34; func main() { var Arrayllist [5] int //定义长度为 3 ，数据类型为int ，名为 Arrayllist 的数组 for i := 0; i \u0026lt; len(Arrayllist); i++ { //遍历数组 fmt.Println(\u0026#34;Arrayllist[\u0026#34;, i, \u0026#34;]: \u0026#34;, Arrayllist[i]) //输出 } } 切片 Slice # Go 语言切片(Slice)是对数组的抽象。\nGo 数组的长度不可改变，在特定场景中这样的集合就不太适用，Go 中提供了一种灵活，功能强悍的内置类型切片(\u0026ldquo;动态数组\u0026rdquo;)，与数组相比切片的长度是不固定的，可以追加元素，在追加时可能使切片的容量增大。\n可以理解为切片是一个有着自动扩容功能的数组。\n相关资料：https://blog.go-zh.org/go-slices-usage-and-internals\n切片的声明 # 切片的声明和数组类似，只不过不需要定义长度。\nvar name []type //声明切片 name : 切片名\ntype : 切片里的数据类型\n因为切片是引用类型，可以使用make函数来声明一个切片。\nvar name []int = make([]type, len) //或者 var name = make([]type, len) //或者 name := make([]type, len) len : 切片初始长度\n也可以指定容量，其中 capacity 为可选参数\nmake([]type, len, capacity) 切片的初始化 # 直接初始化 # var Silce = []int{1, 2, 3, 4, 5} 截取数组进行初始化 # var ArrayList = [5]int{1, 2, 3, 4, 5} //声明数组 ArrayList // ArrayList[startIndex:endIndex] var Silce []int = ArrayList[0:4] //声明Silce,Silce的值是截取数组，第一个到第四个 startIndex : 开始下标(左包含)\nendIndex : 结束下标，但不包含结束下标对应的值(右不包含)\npackage main import \u0026#34;fmt\u0026#34; func main() { var ArrayList = [5]int{1, 2, 3, 4, 5} //声明数组 ArrayList var Silce []int = ArrayList[0:4] //声明Silce,Silce的值是截取数组，第一个到第四个 fmt.Println(Silce) //输出切片 } 输出结果:\n遍历切片 # 和数组一样，切片也是可以使用for循环遍历的:\npackage main import \u0026#34;fmt\u0026#34; func main() { var ArrayList = [5]int{1, 2, 3, 4, 5} //声明数组 ArrayList var Silce []int = ArrayList[0:4] //声明Silce,Silce的值是截取数组，第一个到第四个 for i := 0; i \u0026lt; len(Silce); i++ { //遍历切片 fmt.Println(Silce[i]) } } 补充 # 空切片 # 如果只是单纯的声明一个切片，会导致声明成了一个空(nil)切片，空切片的长度为0，\n需要使用append()函数来给空切片添加值。\npackage main import \u0026#34;fmt\u0026#34; func main() { var Silce_A []int Silce_A = append(Silce_A, []int{1, 2, 3, 4, 5}...) //使用append为空切片添加元素 if Silce_A == nil { //条件判断 fmt.Println(\u0026#34;len(Silce_A) =\u0026#34;, len(Silce_A)) //输出切片长度 } else { fmt.Println(\u0026#34;len(Silce_A) =\u0026#34;, len(Silce_A)) } } 输出结果:\nappend函数 和 copy 函数 # copy 来拷贝切片\nappend 函数用来给切片添加新元素\ncopy函数进行的拷贝是深拷贝的方式，即创建新的内存地址用于存放复制的对象。\n两数组相互独立，不受影响\npackage main import \u0026#34;fmt\u0026#34; func main() { var Silce = []int{1, 2, 3} Silce = append(Silce, 4) //append fmt.Println(\u0026#34;Silce =\u0026#34;, Silce) var SilceCopy = make([]int, len(Silce)) copy(SilceCopy, Silce) //copy if SilceCopy == nil { fmt.Println(\u0026#34;SilceCopy is nil\u0026#34;) } else { fmt.Println(\u0026#34;SilceCopy = \u0026#34;, SilceCopy) } fmt.Println(\u0026#34;-------------------------\u0026#34;) Silce = append(Silce, 5) //给切片Silce添加元素 fmt.Println(\u0026#34;Silce =\u0026#34;, Silce) //输出 fmt.Println(\u0026#34;SilceCopy = \u0026#34;, SilceCopy) } 输出结果:\n使用append来实现删除切片元素 # Go中没有提供专门删除元素的函数，而是通过切片本身的特点来删除元素。\n即以被删除元素为分界点，再利用append将前后两个部分的内存重新连接起来。\nname = append(name[:index], name[index+1:]...) index : 要删除的元素下标\npackage main import \u0026#34;fmt\u0026#34; func main() { var Silce = []int{1, 2, 3, 4, 5} //Silce[:3]只能取到下标为0 1 2 的元素，取不到下标为 3 的元素 //而Silce[4:]... 能取到包含下标为 4 的元素，实际效果就是下标前移 //实现删除的效果 Silce = append(Silce[:3], Silce[4:]...) //删除下标为3的元素 fmt.Println(Silce) } len函数和cap函数 # 切片是可索引的，并且可以由 len() 方法获取长度。\n切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。\npackage main import \u0026#34;fmt\u0026#34; func main() { var ArrayList = [10]int{1, 2, 3, 4, 5, 6, 7, 8} //声明数组ArrayList长度为10 var Silce []int = ArrayList[0:4] //声明Silce,Silce的值是截取数组，第一个到第四个 fmt.Println(\u0026#34;len(Silce) = \u0026#34;, len(Silce)) //获取长度 fmt.Println(\u0026#34;cap(Silce) = \u0026#34;, cap(Silce)) //切片最长可以达到多少 } 输出结果:\n可见如果是截取数组的切片，长度和截取的原数组长度有关。\n如果是声明并且初始化的数组，最大长度是初始长度，而使用append函数添加后，最大长度会自动变化。\npackage main import \u0026#34;fmt\u0026#34; func main() { var Silce = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} fmt.Println(\u0026#34;len(Silce) = \u0026#34;, len(Silce)) //声明并初始化的切片长度 fmt.Println(\u0026#34;cap(Silce) = \u0026#34;, cap(Silce)) //声明并初始化的切片最大长度 Silce = append(Silce, []int{11, 12, 13, 14, 15}...) fmt.Println(\u0026#34;len(Silce)Append = \u0026#34;, len(Silce)) //使用Append添加一次后的切片长度 fmt.Println(\u0026#34;cap(Silce)Append = \u0026#34;, cap(Silce)) //使用Append添加一次后的切片最大长度 Silce = append(Silce, []int{16, 17, 18, 19, 20, 21}...) fmt.Println(\u0026#34;len(Silce)Append2 = \u0026#34;, len(Silce)) //使用Append添加第二次次后的切片长度 fmt.Println(\u0026#34;cap(Silce)Append2 = \u0026#34;, cap(Silce)) //使用Append添加第二次后的切片最大长度 } 输出结果:\nMAP # Go语言中的map是一个无序的key : vlaue 键值对的数据结构容器,map内部实现是哈希表hash。\nmap的特点就是类似与Python的字典，，按照key来找到对应的value 。key指向数据的值。\nMap的声明 # Go语言的map同样是可是使用make来声明的。\nvar Name [KeyType]ValueType //声明map, 此时声明的map是nil map，不存在初始值 //或者 var Name = make(map[KeyType]ValueType) //或者 var Name = map[KeyType]ValueType{key:value,key:value} //这种要静态初始化 Name : 变量名\nKeyType :Key的数据类型\nValueType : Value的数据类型\nMap的初始化 # map的初始化和数组类似。\n但是要直接var Name [KeyType]ValueType 来声明map会得到一个空(nil map)无法被赋值。\n就需要make函数来创建一个非空的map。这样才可以赋值。\npackage main import \u0026#34;fmt\u0026#34; func main() { var Map map[int]int Map = make(map[int]int) //make函数来创建一个非空的map，赋值。 for i := 0; i \u0026lt; 5; i++ { Map[i] = i } fmt.Println(\u0026#34;Map: \u0026#34;, Map) } 输出结果:\n当然也可以利用这个来做个判断：\npackage main import \u0026#34;fmt\u0026#34; func main() { var Map = make(map[int]int) for i := 0; i \u0026lt; 5; i++ { Map[i] = i } Value, err := Map[5] if err == true { //如果能取到kKey为5的值，err为true,否则为false fmt.Println(\u0026#34;Value: \u0026#34;, Value) } else { fmt.Println(\u0026#34;不存在该键\u0026#34;) } fmt.Println(\u0026#34;Map: \u0026#34;, Map) } 遍历Map # 可以使用 for range 来遍历map\n遍历Key package main import \u0026#34;fmt\u0026#34; func main() { var Map = make(map[string]int) Map[\u0026#34;一\u0026#34;] = 1 Map[\u0026#34;二\u0026#34;] = 2 Map[\u0026#34;三\u0026#34;] = 3 Map[\u0026#34;四\u0026#34;] = 4 Map[\u0026#34;五\u0026#34;] = 5 for key := range Map { // 使用for range 遍历key fmt.Println(key) } } 输出结果:\n由于内部实现是哈希值的原因，map不像切片或者数组有序，不存在下标。\n遍历Value package main import \u0026#34;fmt\u0026#34; func main() { var Map = make(map[string]int) Map[\u0026#34;一\u0026#34;] = 1 Map[\u0026#34;二\u0026#34;] = 2 Map[\u0026#34;三\u0026#34;] = 3 Map[\u0026#34;四\u0026#34;] = 4 Map[\u0026#34;五\u0026#34;] = 5 for _, value := range Map { //稍微修改以下即可,key直接用匿名变量 fmt.Println(value) } } 输出结果：\n遍历Key,Value package main import \u0026#34;fmt\u0026#34; func main() { var Map = make(map[string]int) Map[\u0026#34;一\u0026#34;] = 1 Map[\u0026#34;二\u0026#34;] = 2 Map[\u0026#34;三\u0026#34;] = 3 Map[\u0026#34;四\u0026#34;] = 4 Map[\u0026#34;五\u0026#34;] = 5 for key, value := range Map { fmt.Println(key, \u0026#34;: \u0026#34;, value) } } 输出结果:\n删除元素 # elete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。\npackage main import \u0026#34;fmt\u0026#34; func main() { var Map = make(map[int]int) for i := 0; i \u0026lt; 5; i++ { Map[i] = i } delete(Map, 0) //删除key 为 0的元素 delete(Map, 1)//删除key 为 1的元素 delete(Map, 2)//删除key 为 2的元素 for key, value := range Map { fmt.Println(key, \u0026#34;: \u0026#34;, value) //遍历输出 } } 输出结果:\n其他 # 不知道是不是个例，我如果把map类型改为 \\[int\\]int ， 然后遍历key或者value会报错。报错原因是因为window安全中心检测到了病毒。这时候要把实时检测关掉来运行程序，或者卸载Goland重装一遍。\n结构体 Struct # golang有个非常重要的关键字 struct ,也是golang的一个特色.\n结构体特点 # 结构体是一个可以存储不同数据类型的数据类型, 声明完后,它可以和数据类型一样,也有指针值等等.\n定义结构体 # type Person struct { //Person结构体 Age int //参数 Name string Job string Sex bool } 声明结构体 # 结构体可以作为变量声明使用\npackage main import \u0026#34;fmt\u0026#34; type Person struct { Age int Name string Job string Sex bool } func main() { var person Person //创建Person对象 person.Age = 20 //各参数赋值 person.Name = \u0026#34;LiMing\u0026#34; person.Job = \u0026#34;Student\u0026#34; person.Sex = true fmt.Println(person) //输出变量 } 输出结果:\n{20 LiMing Student true} 当然结构体也是可以隐式声明的,类似JAVA语言的实例化\n隐式声明有两种方式\nperson2 := Person{ //隐式声明 Age: 40, Name: \u0026#34;Danny\u0026#34;, Job: \u0026#34;Worker\u0026#34;, Sex: false, } person3 := Person{50, \u0026#34;Jenny\u0026#34;, \u0026#34;Teacher\u0026#34;, true} //隐式声明 注意第二个种声明必须按照顺序给定参数.\n完整代码 :\npackage main import \u0026#34;fmt\u0026#34; type Person struct { Age int Name string Job string Sex bool } func main() { var person Person //创建Person对象 person.Age = 20 //各参数赋值 person.Name = \u0026#34;LiMing\u0026#34; person.Job = \u0026#34;Student\u0026#34; person.Sex = true fmt.Printf(\u0026#34;person: %v\\n\u0026#34;, person) //输出变量 person2 := Person{ //隐式声明 Age: 40, Name: \u0026#34;Danny\u0026#34;, Job: \u0026#34;Worker\u0026#34;, Sex: false, } fmt.Printf(\u0026#34;person2: %v\\n\u0026#34;, person2) person3 := Person{50, \u0026#34;Jenny\u0026#34;, \u0026#34;Teacher\u0026#34;, true} //隐式声明2 fmt.Printf(\u0026#34;person3: %v\\n\u0026#34;, person3) } 输出结果:\nperson: {20 LiMing Student true} person2: {40 Danny Worker false} person3: {50 Jenny Teacher true} 默认声明会给定初始值\ndef := Person{ Age: 0, Name: \u0026#34;\u0026#34;, Job: \u0026#34;\u0026#34;, Sex: false, } 以及一种特殊的声明方法:\nnul := new(Person) nul: \u0026amp;{0 false} 不过这个出来的直接就是带有地址的.\n访问结构体 # 由于 struct 是一种特殊的数据类型,他也可以当做一个数据类型用在函数中\npackage main import \u0026#34;fmt\u0026#34; type Person struct { Age int Name string Job string Sex bool } func main() { person2 := Person{ //隐式声明 Age: 40, Name: \u0026#34;Danny\u0026#34;, Job: \u0026#34;Worker\u0026#34;, Sex: false, } Test(person2) } func Test(person Person) { fmt.Println(person.Name) } 输出结果:\nDanny 接口 Interface # 接口的定义: 接口（interface）定义了一个对象的行为规范，只定义规范不实现，由具体的对象来实现规范的细节。\n接口的使用 # 声明接口\ntype Person interface { //声明接口 Say() //定义方法 Listen() } 实现接口\ntype Person interface { //声明接口 Say() Listen() } type person1 struct { //声明新结构体 Name string Age int } func (p person1) Say() { //实现接口Say fmt.Println(p.Name, \u0026#34;说了一句话\u0026#34;) } func (p person1) Listen() { //实现接口Listen fmt.Println(p.Name, \u0026#34;在听\u0026#34;) } 简单使用\n通过接口实现实例 # package main import \u0026#34;fmt\u0026#34; type Person interface { //声明接口 Say() Listen() } type person1 struct { Name string Age int } func (p person1) Say() { //实现接口 fmt.Println(p.Name, \u0026#34;说了一句话\u0026#34;) } func (p person1) Listen() { fmt.Println(p.Name, \u0026#34;在听\u0026#34;) } func main() { var P Person //接口类型 p2 := person1{ //实例 Name: \u0026#34;person2\u0026#34;, Age: 10, } P = p2 P.Say() //通过接口实现实例内部的函数 P.Listen() } 输出结果:\nperson2 说了一句话 person2 在听 注意的是,只要实现了接口的结构体,均可使用此方法来调用实例的函数,但是通过接口的话无法调用到结构体的内容.\n通过接口实现泛型效果\nfunc Fun(inf interface{}) { //定义函数,将类型设为空接口,即可实现 } 在方法中使用接口\n这样的话,只用将符合接口的实例传入即可调用接口的方法\npackage main import \u0026#34;fmt\u0026#34; type Person interface { //声明接口 Say() Listen() } type person1 struct { Name string Age int } func (p person1) Say() { //实现接口 fmt.Println(p.Name, \u0026#34;说了一句话\u0026#34;) } func (p person1) Listen() { fmt.Println(p.Name, \u0026#34;在听\u0026#34;) } func main() { var P Person p2 := person1{ Name: \u0026#34;person2\u0026#34;, Age: 10, } P = p2 P.Say() P.Listen() p3 := person1{ Name: \u0026#34;person3\u0026#34;, Age: 11, } Fun(p3) } func Fun(per Person) { //定义函数,将类型设为空接口,即可实现 per.Say() per.Listen() } 输出结果:\nperson2 说了一句话 person2 在听 person3 说了一句话 person3 在听 其他 # Go语言接口 和 JAVA接口 的区别 java 实现结果需要implement 关键词来指定实现具体接口， 而golang的话只需要与某个接口中的某个方法名字相同即可，golang更加松散.\n通道 Channel # Channel的主要使用是在Golang 的并发中: 相关笔记:Golang并发编程\n函数 Func # 何为函数 # 函数是基本的代码块，用于执行一个任务。\nGo 语言最少有个 main() 函数。\n你可以通过函数来划分不同功能，逻辑上每个函数执行的是指定的任务。\n函数声明告诉了编译器函数的名称，返回类型，和参数。\n函数的定义和调用 # 函数定义 # Go语言定义函数的语法：\nfunc function_name( [parameter list] ) [return_types] { 函数体 [return return_Numbers] } func ：定义函数的关键字\nfunction_name : 函数名\nparameter list : 参数列表，需要传入的参数和参数类型，根据需要选择可以不设置。\nreturn_types ： 返回值类型，返回一系列的值，根据需要选择可以不设置。\n函数体：函数定义的代码集合。\nfunc PrintHello() { //打印Hello的函数 var String = \u0026#34;Hello\u0026#34; fmt.Println(String) } 函数调用 # 当创建函数时，你定义了函数需要做什么，通过调用该函数来执行指定任务。\npackage main import \u0026#34;fmt\u0026#34; func main() { PrintHello() //调用PrintHello函数 fmt.Println(PrintHelloWorld()) //调用PrintHelloWorld函数 fmt.Println(Add(10, 10)) //调用求和函数 } func PrintHello() { //PrinrHello函数，无参，无返回值 var String = \u0026#34;Hello\u0026#34; fmt.Println(String) } func PrintHelloWorld() string { //PrintHelloWorld函数，无参，有返回值 var String = \u0026#34;Hello,World!\u0026#34; return String } func Add(X int, Y int) int { //求和函数，有参，有返回值 Res := X + Y return Res } 运行结果：\n函数类型 # 使用Type关键字可以定义函数类型。\n在Go语言中,type可以定义任何自定义的类型。\npackage main import \u0026#34;fmt\u0026#34; func main() { type T1 func(int, int) int //使用type关键字，定义T1类型 var F T1 = Add //声明T1类型的变量F ，将Add函数赋给F f1 := F(1, 4) fmt.Println(f1) var F2 T1 = subtract f2 := F2(10, 40) fmt.Println(f2) fmt.Println(f1 + f2) } func Add(X int, Y int) int { //Add函数 Res := X + Y return Res } func subtract(X int, Y int) int { //subtract函数 Res := X - Y return Res } 输出结果：\n高阶函数 # 函数作为参数 # package main import \u0026#34;fmt\u0026#34; func main() { PrintHello(\u0026#34;Sam\u0026#34;, SayHello) } func SayHello(name string) string { var Hello string = name + \u0026#34; Hello\u0026#34; fmt.Println(\u0026#34;func SayHello: \u0026#34;, Hello) return Hello } func PrintHello(name string, f func(string2 string) string) { fmt.Println(\u0026#34;func PrintHello: \u0026#34;, name, f(name)) } 输出结果：\n函数作为返回值 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { fmt.Println(Add(10, 20)) } func Add(x int, y int) string { var Res = x + y return ToString(Res) } func ToString(z int) string { var Res = strconv.Itoa(z) //使用 strconv.Itoa()将整型数字转为字符串数字 return Res } 匿名函数 # Go语言中的函数无法实现函数的嵌套，但是可以通过匿名函数来实现相似的效果。\n匿名函数效果类似Python中的lamda\n格式:\nfunc(参数列表) 返回值 { 函数体 } package main import \u0026#34;fmt\u0026#34; func main() { Add := func(X int, Y int) int { //匿名函数 return X + Y } A := Add(1, 2) //调用匿名函数 fmt.Println(\u0026#34;var A =\u0026#34;, A) //输出 } 输出结果：\nvar A = 3\n其他 # strconv.Itoa() 和 string()的区别\nstring()是直接整数类型的数字转为ASCII码值等于该整形数字的字符。\n而strconv.Itoa() 是转换成对应的字符串类型的数字。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { var Number = 40 fmt.Println(Number) fmt.Printf(\u0026#34;%T\\n\u0026#34;, Number) fmt.Println(\u0026#34;------------------\u0026#34;) fmt.Println(string(Number)) fmt.Printf(\u0026#34;%T\\n\u0026#34;, string(Number)) fmt.Println(\u0026#34;------------------\u0026#34;) fmt.Println(strconv.Itoa(Number)) fmt.Printf(\u0026#34;%T\\n\u0026#34;, strconv.Itoa(Number)) } 输出结果：\n","date":"2022 年 11 月 30 日","externalUrl":null,"permalink":"/posts/golang%E6%B4%BE%E7%94%9F%E7%B1%BB%E5%9E%8B/","section":"Posts","summary":"","title":"Golang派生类型","type":"posts"},{"content":"","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/tags/%E5%8F%8B%E9%93%BE/","section":"Tags","summary":"","title":"友链","type":"tags"},{"content":" 学长 # Coder的笔记 朋友们 # Vincent文朔：太阳出来了，雾就会散的。\n二次元の博客：生命不息，折腾不止！\n北熙宝宝：如果恋爱有颜色，那一定是纯白色！\n心盲博客：人生苦短，行走在人生路上，总会有许多得失和起落！\n糖宝の小窝：你可能不是最棒的，但你一定是最胖的！\n长生风月：关关难过关关过，前路漫漫亦灿灿\n学习链接 # 刘丹冰Aceld · 语雀 (yuque.com)\n友链申请须知 # 本站点不接受 内容全为转载文章的站点、查看或下载内容需付费、拥有造成访问者威胁的脚本、含有大量广告的站点\n头像链接与站点链接须已部署 SSL 证书\n申请格式:\n站点名称: 站点网址: 头像链接: 介绍自己的一句话\u0026amp;自己喜欢的一句话： 我的信息:\n站点名称：Xenolies Blog 站点网址: https://xenolies.top/ 头像链接: https://xenolies.top/site/logo.svg 简介 ：Keep on keeping on. ","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/links/","section":"Xenolies Blog","summary":"","title":"友情链接","type":"page"},{"content":" 我是谁？ # 00后，大专学过编程，接触过 Java / Go / Python，目前在工厂做一线工作。\n白天上班，晚上折腾代码和各种奇怪项目。\n对复古游戏开发、嵌入式和 AI 都有兴趣：\n用 PSY-Q 在 Windows 上折腾 PS1 自制 3D Demo\n玩过 ESP32、墨水屏等小硬件\n数学和硬件基础都不强，很多东西是边查资料边踩坑学的。 这个博客主要记录我的学习过程、项目复盘和一些失败经验，希望能留下成长轨迹，也希望能帮到和我一样起点普通的人。\n我这奇怪 ID 的由来。 # 我的 ID 为 Xenolies，整体看起来很奇怪。\n其实把它拆开看就好理解一些了。Xeno 和 Lies\n其中 Xeno 这个词呢，是取自我最喜欢的 JRPG “异度” 系列的 （时空勇士 (LIVE A LIVE) 都 HD-2D 重制了，SE 也不重制一下这个 55555….）\nlies 这个就是觉得很好听，当时想后半截的时候想到了 Soild Snake 觉得 Snake 有些好听，就整了个类似的.\nTimeLine # Python 入门 2019 初次接触 这一年我初次接触 Python，被其简洁的语法吸引，开始编写简单的脚本，开启了编程的大门。 系统学习与多语言 2021 Java \u0026amp; Golang 开始系统地构建知识体系。同时，视野拓宽，开始接触 Java 和 Go 语言，并初识 Arduino 和 ESP32。学习HTML、CSS和JavaScript，构建基础的前端页面。 博客与云原生 2022 Linux \u0026amp; Docker 建立了个人博客，记录学习笔记。深入学习 Go 并发、Gin 框架，同时接触 Linux 和 Docker。 沉淀与回归 2023 进厂普工 这一年选择进入工厂成为普工，虽然技术上看似暂停，但这是人生宝贵的沉淀期。 AI 与复古开发 2025 深度学习 \u0026amp; PSYQ 树莓派 和 深度学习 入门！同时接触 PSY-Q 复古开发，探索计算机底层原理。 线性代数与图形学 2026 新征程 今年开始学习 线性代数 和 计算机图形学 入门，补齐底层数学与图形渲染知识。 ","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/about/","section":"Xenolies Blog","summary":"","title":"关于我","type":"page"},{"content":" 数据库连接和驱动 # 安装数据库驱动包\ngo get -u github.com/go-sql-driver/mysql 导入数据驱动\nimport ( \u0026#34;database/sql\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; //引用包内部分函数 ) sql.DB 结构体\nsql.DB结构是sql/database包封装的一个数据库操作对象，包含了操作数据库的基本方法。\nDSN\nDSN全称为Data Source Name，表示数据库连来源，用于定义如何连接数据库，不同数据库的DSN格式是不同的，这取决于数据库驱动的实现,\n下面是 go-sql-driver/sql 的DSN格式：\n//[用户名[:密码]@][协议(数据库服务器地址)]]/数据库名称?参数列表 [username[:password]@][protocol[(address)]]/dbname[?param1=value1\u0026amp;...\u0026amp;paramN=valueN] 连接数据库 # 在 sql/database包中.使用 sql.Open来连接数据库,值得注意的是 sql.Open 这样连接不会验证密码和账户是否正确,数据库是否存在,只会校验 DSN 格式.\ndrever := \u0026#34;mysql\u0026#34; //root为数据库用户名，后面为密码，tcp代表tcp协议，test处填写自己的数据库名称 DSN := \u0026#34;root:123123@tcp(127.0.0.1:3306)/godatabase?charset=utf8\u0026#34; db, err := sql.Open(drever, DSN) if err != nil { //不会校验用户名密码是否正确，只校验数据源格式 fmt.Printf(\u0026#34;sql.Open err: %v\u0026#34;, err) //dsn格式不正确时报错 return } 验证账号密码以及要连接的数据库是否正确 使用 sql.Ping() 可以检验账号密码和数据库是否存在\nerr = db.Ping() //用来测试账号密码 if err != nil { fmt.Printf(\u0026#34;db.Ping err: %v\u0026#34;, err) return } 完整代码:\npackage main import ( \u0026#34;database/sql\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; \u0026#34;fmt\u0026#34;) type info struct { id int `db.id` name string `db.name` password int `db.password` } func main() { drever := \u0026#34;mysql\u0026#34; //root为数据库用户名，后面为密码，tcp代表tcp协议，test处填写自己的数据库名称 DSN := \u0026#34;root:123123@tcp(127.0.0.1:3306)/godatabase?charset=utf8\u0026#34; db, err := sql.Open(drever, DSN) if err != nil { //不会校验用户名密码是否正确，只校验数据源格式 fmt.Printf(\u0026#34;sql.Open err: %v\u0026#34;, err) //dsn格式不正确时报错 return } err = db.Ping() //用来测试账号密码 if err != nil { fmt.Printf(\u0026#34;db.Ping err: %v\u0026#34;, err) return } db.SetMaxOpenConns(5) //设置最大数据库连接数 fmt.Println(\u0026#34;连接数据库成功！\u0026#34;) } 输出结果:\n连接数据库成功！ 数据库操作 # 查询数据库 # 使用 sql.Query() 即可查询,和JAVA相同的是 都要传入SQL语句并且返回一个结果集.\n而从结果集读出信息需要创建查询数据表对应的结构体,还需要用到读取结果集的语句 sql.Scan() ,值得注意的是,传入的结构体变量要转为指针,\n结构体:\ntype info struct { id int `db.id` name string `db.name` password int `db.password` } 查询方法代码:\nfunc dbQuery(db *sql.DB, tableName string, inFo info) { SQL := \u0026#34;SELECT * FROM \u0026#34; + tableName //SQL语句 rows, err := db.Query(SQL) //获得结果集 if err != nil { //捕获错误 fmt.Printf(\u0026#34;db.Query err: %v\u0026#34;, err) return } for rows.Next() { err := rows.Scan(\u0026amp;inFo.id, \u0026amp;inFo.name, \u0026amp;inFo.password) //从结果集中读取 if err != nil { fmt.Printf(\u0026#34;rows.Scan err: %v\u0026#34;, err) return } fmt.Println(inFo) //输出 } rows.Close() } 完整代码:\npackage main import ( \u0026#34;database/sql\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; \u0026#34;fmt\u0026#34;) type info struct { id int `db.id` name string `db.name` password int `db.password` } func main() { drever := \u0026#34;mysql\u0026#34; //root为数据库用户名，后面为密码，tcp代表tcp协议，test处填写自己的数据库名称 DSN := \u0026#34;root:123123@tcp(127.0.0.1:3306)/godatabase?charset=utf8\u0026#34; db, err := sql.Open(drever, DSN) if err != nil { //不会校验用户名密码是否正确，只校验数据源格式 fmt.Printf(\u0026#34;sql.Open err: %v\u0026#34;, err) //dsn格式不正确时报错 return } err = db.Ping() //用来测试账号密码 if err != nil { fmt.Printf(\u0026#34;db.Ping err: %v\u0026#34;, err) return } db.SetMaxOpenConns(5) //设置最大数据库连接数 fmt.Println(\u0026#34;连接数据库成功！\u0026#34;) fmt.Println(\u0026#34;查询数据库\u0026#34;) var iNFO info dbQuery(db, \u0026#34;userinfo\u0026#34;, iNFO) } func dbQuery(db *sql.DB, tableName string, inFo info) { SQL := \u0026#34;SELECT * FROM \u0026#34; + tableName rows, err := db.Query(SQL) //获得结果集 if err != nil { fmt.Printf(\u0026#34;db.Query err: %v\u0026#34;, err) return } for rows.Next() { err := rows.Scan(\u0026amp;inFo.id, \u0026amp;inFo.name, \u0026amp;inFo.password) if err != nil { fmt.Printf(\u0026#34;rows.Scan err: %v\u0026#34;, err) return } fmt.Println(inFo) } rows.Close() } 输出结果:\n连接数据库成功！ 查询数据库 {1 123 123123} {2 1234 123123} 此外还有两两种常用操作:\nQueryRow表示只返回一行的查询，作为Query的一个常见特例。 Prepare表示准备一个需要多次使用的语句，供后续执行用。(预编译) 增加数据库条目 # 增加新条目需要使用 sql.Exec() 方法来实现,当然,也是要传入SQL语句的.\nfunc dbExec(db *sql.DB) { SQL := \u0026#34;INSERT INTO userinfo(name,password) VALUES (?,?)\u0026#34; //我的数据表ID设置为自增,所以设置了两个值 , err := db.Exec(SQL, 12, 1234) //添加新条目 ,会返回err if err != nil { fmt.Printf(\u0026#34;db.Exec err: %v\u0026#34;, err) return } } 输出结果:\n连接数据库成功！ 查询数据库 {1 123 123123} {2 1234 123123} 添加新条目 再次查询 {1 123 123123} {2 1234 123123} {4 12 1234} 更新数据库 # 数据库的更新也是和添加数据库条目相同,使用 sql.Exec() + SQL语句即可查询.\nfunc dbUpdate(db *sql.DB) { SQL := \u0026#34;UPDATE userinfo SET name where id=1\u0026#34; _, err := db.Exec(SQL, \u0026#34;啊啊\u0026#34;, 1) if err != nil { fmt.Printf(\u0026#34;db Update err: %v\u0026#34;, err) return } } 输出结果:\n连接数据库成功！ 查询数据库 {1 123 123123} {2 1234 123123} 添加新条目 再次查询 {1 123 123123} {2 1234 123123} {6 12 1234} 更新后查询 {1 啊啊 123123} {2 1234 123123} {6 12 1234} 删除数据库条目 # 数据库条目的删除也是和添加数据库条目,更新数据库相同,使用 sql.Exec() + SQL语句即可查询.\nfunc dbDelete(db *sql.DB) { SQL := \u0026#34;DELETE FROM userinfo WHERE id= ? \u0026#34; _, err := db.Exec(SQL, 7) if err != nil { fmt.Printf(\u0026#34;db Delete err: %v\u0026#34;, err) return } } 输出结果:\n连接数据库成功！ 查询数据库 {1 啊啊 123123} {2 1234 123123} {7 1234 1232} 添加新条目 添加后查询 {1 啊啊 123123} {2 1234 123123} {7 1234 1232} {8 12 1234} 更新数据库 更新后查询 {1 啊啊 123123} {2 1234 123123} {7 1234 1232} {8 12 1234} 删除数据库条目 删除后查询 {1 啊啊 123123} {2 1234 123123} {8 12 1234} 完整代码:\npackage main import ( \u0026#34;database/sql\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; \u0026#34;fmt\u0026#34;) type info struct { id int `db.id` name string `db.name` password int `db.password` } func main() { drever := \u0026#34;mysql\u0026#34; //root为数据库用户名，后面为密码，tcp代表tcp协议，test处填写自己的数据库名称 DSN := \u0026#34;root:123123@tcp(127.0.0.1:3306)/godatabase?charset=utf8\u0026#34; db, err := sql.Open(drever, DSN) if err != nil { //不会校验用户名密码是否正确，只校验数据源格式 fmt.Printf(\u0026#34;sql.Open err: %v\u0026#34;, err) //dsn格式不正确时报错 return } err = db.Ping() //用来测试账号密码 if err != nil { fmt.Printf(\u0026#34;db.Ping err: %v\u0026#34;, err) return } db.SetMaxOpenConns(5) //设置最大数据库连接数 fmt.Println(\u0026#34;连接数据库成功！\u0026#34;) fmt.Println(\u0026#34;查询数据库\u0026#34;) var iNFO info dbQuery(db, \u0026#34;userinfo\u0026#34;, iNFO) fmt.Println(\u0026#34;添加新条目\u0026#34;) dbExec(db) fmt.Println(\u0026#34;添加后查询\u0026#34;) dbQuery(db, \u0026#34;userinfo\u0026#34;, iNFO) fmt.Println(\u0026#34;更新数据库\u0026#34;) dbUpdate(db) fmt.Println(\u0026#34;更新后查询\u0026#34;) dbQuery(db, \u0026#34;userinfo\u0026#34;, iNFO) fmt.Println(\u0026#34;删除数据库条目\u0026#34;) dbDelete(db) fmt.Println(\u0026#34;删除后查询\u0026#34;) dbQuery(db, \u0026#34;userinfo\u0026#34;, iNFO) } func dbQuery(db *sql.DB, tableName string, inFo info) { SQL := \u0026#34;SELECT * FROM \u0026#34; + tableName rows, err := db.Query(SQL) //获得结果集 if err != nil { fmt.Printf(\u0026#34;db.Query err: %v\u0026#34;, err) return } for rows.Next() { err := rows.Scan(\u0026amp;inFo.id, \u0026amp;inFo.name, \u0026amp;inFo.password) if err != nil { fmt.Printf(\u0026#34;rows.Scan err: %v\u0026#34;, err) return } fmt.Println(inFo) } rows.Close() } func dbExec(db *sql.DB) { SQL := \u0026#34;INSERT INTO userinfo(name,password) VALUES (?,?)\u0026#34; _, err := db.Exec(SQL, 12, 1234) if err != nil { fmt.Printf(\u0026#34;db.Exec err: %v\u0026#34;, err) return } } func dbUpdate(db *sql.DB) { SQL := \u0026#34;UPDATE userinfo SET name=? where id=?\u0026#34; _, err := db.Exec(SQL, \u0026#34;啊啊\u0026#34;, 1) if err != nil { fmt.Printf(\u0026#34;db Update err: %v\u0026#34;, err) return } } func dbDelete(db *sql.DB) { SQL := \u0026#34;DELETE FROM userinfo WHERE id= ? \u0026#34; _, err := db.Exec(SQL, 7) if err != nil { fmt.Printf(\u0026#34;db Delete err: %v\u0026#34;, err) return } } 其他 # MySQL事务 # 不过这这样一条一条的增删改查是非常繁琐的 ,这样高重复的 MySQL ,就构成了一个 事务. 具体的SQL事务,可以看这篇文章: MySQL 事务 | 菜鸟教程 (runoob.com)\n这里简单摘抄一句 MySQL事务 :\nMySQL 事务主要用于处理操作量大，复杂度高的数据。比如说，在人员管理系统中，你删除一个人员，你既需要删除人员的基本资料，也要删除和该人员相关的信息，如信箱，文章等等，这样，这些数据库操作语句就构成一个事务！\n","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/posts/golang-%E6%93%8D%E4%BD%9C%E6%95%B0%E6%8D%AE%E5%BA%93/","section":"Posts","summary":"","title":"Golang 操作数据库","type":"posts"},{"content":"Go 语言支持并发，我们只需要通过 go 关键字来开启 goroutine 即可。 goroutine 是轻量级线程，goroutine 的调度是由 Golang 运行时进行管理的。\n并行和并发 # 并行：在同一时刻，有多条指令在多个CPU处理器上同时执行 2个队伍，2个窗口，要求硬件支持 并发：在同一时刻，只能有一条指令执行，但多个进程指令被快速地轮换执行 2个队伍，1个窗口，要求提升软件能力 Go语言并发优势 # go从语言层面就支持了并发 简化了并发程序的编写 Goroutine # Goroutine 是go并发设计的核心 Goroutine 就是协程，它比线程更小，十几个Goroutine 在底层可能就是五六个线程 go语言内部实现了Goroutine e的内存共享，执行Goroutine 只需极少的栈内存(大概是4~5KB) 使用Goroutine\ngo 函数名( 参数列表 ) 使用如下:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34;) func running() { var times int // 构建一个无限循环 for { times++ fmt.Println(\u0026#34;tick\u0026#34;, times) // 延时1秒 time.Sleep(time.Second) } } func main() { // 并发执行程序 go running() // 接受命令行输入, 不做任何事情 var input string fmt.Scanln(\u0026amp;input) } 输出结果:\ntick 1 tick 2 tick 3 123tick 4 123tick 5 132tick 6 113tick 7 1313tick 8 代码执行后，命令行会不断地输出 tick，同时可以使用 fmt.Scanln() 接受用户输入。两个环节可以同时进行。\nGo 程序在启动时，运行时（runtime）会默认为 main() 函数创建一个 goroutine。\n在 main() 函数的 goroutine 中执行到 go running 语句时，归属于 running() 函数的 goroutine 被创建，running() 函数开始在自己的 goroutine 中执行。\n此时，main() 继续执行，两个 goroutine 通过 Go 程序的调度机制同时运作。\nGoroutine 管理 # sync.WaitGroup # 如果几个 Goroutine 需要执行,且每个 Goroutine 没有先后执行限制,可以引用 sync.WaitGroup 来处理这样的情况.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34;) func main() { var wg sync.WaitGroup wg.Add(3) go funA(\u0026amp;wg) go funB(\u0026amp;wg) go funC(\u0026amp;wg) wg.Wait() fmt.Println(\u0026#34;Finish\u0026#34;) } func funA(wg *sync.WaitGroup) { defer wg.Done() fmt.Println(\u0026#34;FunA\u0026#34;) } func funB(wg *sync.WaitGroup) { defer wg.Done() fmt.Println(\u0026#34;FunB\u0026#34;) } func funC(wg *sync.WaitGroup) { defer wg.Done() fmt.Println(\u0026#34;FunC\u0026#34;) } 输出结果:\nFunC FunB FunA Finish 需要注意的是 sync.WaitGroup 的操作的是sync.WaitGroup中的地址,需要传入 \u0026amp;wg\n通道 Channel # 而如果 Goroutine 之间需要数据沟通,就需要使用 通道 (Channel) 来实现 Goroutine 之间的数据通信 . 你可以把它看成一个管道.通过它并发核心单元就可以发送或者接收数据进行通讯(Communication)\nChannel 是 Golang 在语言级别提供的 Goroutine 之间的通信方式，可以使用 Channel 在两个或多个 Goroutine 之间传递消息。\n声明通道 # 通道类型和普通变量类型的声明区别,仅仅是加了个 chan 关键字 .\nvar ch chan int //声明一个名为 ch ,类型为int 的channel 在 Golang 中使用 Make关键字来创建 Channel实例\nch := make(chan int) 给Channel赋值也相当简单.它的操作符是箭头 \u0026lt;- ,箭头的指向就是数据的流向.\nch \u0026lt;- v // 发送值v到Channel ch中 v := \u0026lt;- ch // 从Channel ch中接收数据，并将数据赋值给v 无缓冲 Channel # 无缓冲的 Channel(unbuffered channel) 是指在接收前没有能力保存任何值的 channel。\n这种类型的 Channel 要求发送 Goroutine 和接收 Goroutine 同时准备好，才能完成发送和接收操作。\n如果两个 Goroutine 没有同时准备好，Channel 会导致先执行发送或接收操作的 Goroutine 阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。\n无缓冲 Channel 传递情况如图: 实例:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34;) func main() { ch := make(chan int) //这里就是创建了一个无缓冲Channel go write(ch) //写入通道的Goroutine go read(ch) //读出通道的Goroutine time.Sleep(1) close(ch) //使用后关闭 } func write(ch chan int) { for i := 0; i \u0026lt; 6; i++ { ch \u0026lt;- i //循环写入管道 fmt.Println(\u0026#34;写入\u0026#34;, i) } } func read(ch chan int) { for i := 0; i \u0026lt; 6; i++ { //主go程 num := \u0026lt;-ch //循环读出管道 fmt.Println(\u0026#34;读出\u0026#34;, num) } } 输出结果:\n读出 0 写入 0 写入 1 读出 1 读出 2 写入 2 写入 3 读出 3 读出 4 写入 4 写入 5 读出 5 也由此可见,无缓冲的Channel是即写即读,能保证并发的数据统一.\n但是问题在于只能存在一个值,如果大量的值要写入到通道,就需要带缓冲的Channel来解决这个问题了.\n带缓冲 Channel # 带缓冲的 Channel(buffered channel) 是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 Goroutine 之间必须同时完成发送和接收。\n通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时，接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时，发送动作才会阻塞。\n这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同：\n无缓冲的通道保证进行发送和接收的 Goroutine 会在同一时间进行数据交换；有缓冲的通道没有这种保证。\n缓冲区效果如下:\npackage main import \u0026#34;fmt\u0026#34; func main() { ch := make(chan int, 2) //创建有两个缓冲区的Channel ch \u0026lt;- 11 //给Channel传入值 ch \u0026lt;- 12 close(ch) //关闭Channel,此时Channel值存在11和12两个值 for x := range ch { //遍历Channel fmt.Println(\u0026#34;x: \u0026#34;, x) //读出Channel的值 } x, b := \u0026lt;-ch //再次读出Channel中的值,此时Channel中的值全部被读出,Channel为0,且会返回一个False说明Channel没有值了 fmt.Println(x, b) } 输出结果:\nx: 11 x: 12 0 false 带缓冲 Channel 传递情况如图: 实例代码如下:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34;) func main() { ch := make(chan int, 10) //创建有10个缓冲区的Channel go write(ch) time.Sleep(100) //休眠 go read(ch) time.Sleep(1000) close(ch) } func write(ch chan int) { for i := 0; i \u0026lt; 6; i++ { ch \u0026lt;- i //循环写入管道 fmt.Println(\u0026#34;写入\u0026#34;, i) } } func read(ch chan int) { for i := 0; i \u0026lt; 6; i++ { //主go程 num := \u0026lt;-ch //循环读出管道 fmt.Println(\u0026#34;读出\u0026#34;, num) } } 输出结果:\n写入 0 写入 1 写入 2 写入 3 写入 4 写入 5 读出 0 读出 1 读出 2 读出 3 读出 4 读出 5 这里我们发现,数据不是即写即读了,说明数据都存在缓冲区了.\n其他 # 如何理解线程, 进程以及协程 一文读懂什么是进程、线程、协程 - 腾讯云开发者社区-腾讯云 (tencent.com)\n本文简言之就是:\n进程 # 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程，是操作系统进行资源分配和调度的一个独立单位，是应用程序运行的载体。进程是一种抽象的概念，从来没有统一的标准定义。\n线程 # 线程是程序执行中一个单一的顺序控制流程，是程序执行流的最小单元，是处理器调度和分派的基本单位。一个进程可以有一个或多个线程，各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。\n协程 # 线程的切换由操作系统负责调度，协程由用户自己进行调度，因此减少了上下文切换，提高了效率。 线程的默认Stack大小是1M，而协程更轻量，接近1K。因此可以在相同的内存中开启更多的协程。 由于在同一个线程上，因此可以避免竞争关系而使用锁。 适用于被阻塞的，且需要大量并发的场景。但不适用于大量计算的多线程，遇到此种情况，更好实用线程去解决。 ","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/posts/golang%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/","section":"Posts","summary":"","title":"Golang并发编程","type":"posts"},{"content":" 协议 # 协议可以理解为规则，是数据传输和解释的规则，是通信双方都要遵守的规则。\n协议存在的意义是为了让双方更好的沟通。\n在双方之间被遵守的协议成为原始协议。\n当此协议被更多的人采用后，不断的完善，最终形成一个稳定的、完整的文件传输协议，被广泛应用于各种文件传输过程中。该协议就成为了一个标准协议。\n网络应用模型 # C/S模型 : 需要在服务器端和客户端都安装部署，才能完成数据通信。性能好，因为可以提前把数据缓存到客户端本地，提高用户体验，限制也少，可采用的协议相对灵活。\nB/S模型：只需要在服务器端安装部署，客户端只需要一个浏览器即可。优势是移植性非常好，不收平台限制，只需一个浏览器就可以打开。缺点是无法想C/S那样提前缓存大量数据在本地，网络受限时，应用的体验感非常差，而且浏览器采用的协议是标准http协议通信，没有C/S灵活 .\n标准库net包基本使用 # TCP编程 # net库是Golang原生自带标准库，无需第三方包来支持网络功能。\n服务器端基本代码\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) func main() { listener, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;10.205.6.142:8088\u0026#34;) if err != nil { fmt.Printf(\u0026#34;net.Listen error: %v\u0026#34;, err) return } fmt.Println(\u0026#34;服务器已开启...\u0026#34;) defer listener.Close() //设置延迟关闭,防止服务器关闭 for { //设置阻塞持续监听 conn, err := listener.Accept() if err != nil { fmt.Printf(\u0026#34;listener.Accept error: %v\u0026#34;, err) return } else { fmt.Println(\u0026#34;服务器: \u0026#34;, conn.LocalAddr().String()) } } } 链接验证:\n在Windows功能里面开启telnet 功能,然后CMD窗口输入 telnet 本机IP 监听的端口号(注意:IP和端口号中间为空格),即可链接.\n客户端基本代码\n客户端是用net.Dial()来进行链接.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) func main() { fmt.Println(\u0026#34;客户端开始...\u0026#34;) conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;10.205.6.142:8088\u0026#34;) //链接服务器端口 if err != nil { fmt.Printf(\u0026#34;net.Dial error: %v\u0026#34;, err) return } defer conn.Close() fmt.Println(\u0026#34;客户链接成功\u0026#34;) fmt.Println(\u0026#34;服务器: \u0026#34;, conn.RemoteAddr().String()) //输出服务器和IP和端口号结束 fmt.Println(\u0026#34;客户端: \u0026#34;, conn.LocalAddr().String()) } 输出结果:\n服务器端:\n客户端:\n数据交互 # 客户端 # 数据交互需要调用 os.Stdin 来实现检测\nbufio来实现IO流，进而实现数据传递。\npackage main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; ) func main() { fmt.Println(\u0026#34;客户端开始...\u0026#34;) conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;10.205.6.142:8088\u0026#34;) //链接服务器端口 if err != nil { fmt.Printf(\u0026#34;net.Dial error: %v\u0026#34;, err) return } defer conn.Close() fmt.Println(\u0026#34;客户端链接成功\u0026#34;) Wirte(conn) fmt.Println(\u0026#34;服务器: \u0026#34;, conn.RemoteAddr().String()) //输出服务器和IP和端口号结束 fmt.Println(\u0026#34;客户端: \u0026#34;, conn.LocalAddr().String()) } func Wirte(conn net.Conn) { fmt.Print(\u0026#34;请输入: \u0026#34;) reader := bufio.NewReader(os.Stdin) //标准输入流 line, err := reader.ReadString(\u0026#39;\\n\u0026#39;) if err != nil { fmt.Printf(\u0026#34;reader.ReadStrin error: %v\u0026#34;, err) return } len, err := conn.Write([]byte(line)) if err != nil { fmt.Printf(\u0026#34;conn.Write error: %v\u0026#34;, err) return } fmt.Printf(\u0026#34;写了 %v 字节\\n\u0026#34;, len) } 输出结果:\n服务器端 # 向对应写一个输入流来持续接收客户端发送的数据\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) func main() { listener, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;10.205.6.142:8088\u0026#34;) if err != nil { fmt.Printf(\u0026#34;net.Listen error: %v\u0026#34;, err) return } fmt.Println(\u0026#34;服务器已开启...\u0026#34;) defer listener.Close() //设置延迟关闭,防止服务器关闭 for { //设置阻塞持续监听 conn, err := listener.Accept() if err != nil { fmt.Printf(\u0026#34;listener.Accept error: %v\u0026#34;, err) return } else { fmt.Println(\u0026#34;服务器: \u0026#34;, conn.LocalAddr().String()) get(conn) //获取客户端数据 } } } // 获取客户端输入 func get(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { fmt.Printf(\u0026#34;conn.Read error: %v\u0026#34;, err) return } fmt.Println(n) } } 但此时会发现有个EOF的错误\n这个错误是说明Cilent端(客户端)此时已经退出了(关闭了)。\n可以专门优化一下:\nif err == io.EOF { //处理EOF　客户端断开error fmt.Printf(\u0026#34;客户端(%v)已断开\u0026#34;, conn.RemoteAddr()) return } 此时观察接收到的数据，返回的是４\n对比客户端，\n发现是返回的写入字节大小。所以就需要转为string\nfmt.Println(string(buf[:n])) 整体完整代码 # 服务器完整代码\n/** * TCP服务器端 */ package main import ( \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;net\u0026#34; ) func main() { listener, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8808\u0026#34;) if err != nil { fmt.Printf(\u0026#34;net.Listen error: %v\u0026#34;, err) return } fmt.Println(\u0026#34;服务器已开启...\u0026#34;) defer listener.Close() //设置延迟关闭,防止服务器关闭 for { //设置阻塞持续监听 conn, err := listener.Accept() go get(conn) if err != nil { fmt.Printf(\u0026#34;listener.Accept error: %v\u0026#34;, err) return } else { fmt.Println(\u0026#34;服务器: \u0026#34;, conn.LocalAddr().String()) } } } // 获取客户端输入 func get(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { if err == io.EOF { //处理EOF　客户端断开error fmt.Printf(\u0026#34;客户端(%v)已断开\u0026#34;, conn.RemoteAddr()) return } fmt.Printf(\u0026#34;conn.Read error: %v\u0026#34;, err) return } fmt.Printf(\u0026#34;接收到客户端输入: %v\u0026#34;, string(buf[:n])) } } 客户端完整代码\n/** * TCP客户端 */ package main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; ) func main() { fmt.Println(\u0026#34;客户端开始...\u0026#34;) conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8808\u0026#34;) //链接服务器端口 if err != nil { fmt.Printf(\u0026#34;net.Dial error: %v\u0026#34;, err) return } defer conn.Close() fmt.Println(\u0026#34;客户端链接成功\u0026#34;) Wirte(conn) fmt.Println(\u0026#34;服务器: \u0026#34;, conn.RemoteAddr().String()) //输出服务器和IP和端口号结束 fmt.Println(\u0026#34;客户端: \u0026#34;, conn.LocalAddr().String()) } func Wirte(conn net.Conn) { fmt.Print(\u0026#34;请输入: \u0026#34;) reader := bufio.NewReader(os.Stdin) //标准输入流 line, err := reader.ReadString(\u0026#39;\\n\u0026#39;) if err != nil { fmt.Printf(\u0026#34;reader.ReadStrin error: %v\u0026#34;, err) return } len, err := conn.Write([]byte(line)) if err != nil { fmt.Printf(\u0026#34;conn.Write error: %v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;写了 %v 字节\\n\u0026#34;, len) } TCP粘包 # 以这样的TCP代码为例\n服务端\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) func main() { fmt.Println(\u0026#34;客户端开始...\u0026#34;) conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8808\u0026#34;) //链接服务器端口 if err != nil { fmt.Printf(\u0026#34;net.Dial error: %v\u0026#34;, err) return } defer conn.Close() fmt.Println(\u0026#34;客户端链接成功\u0026#34;) Wirte(conn) fmt.Println(\u0026#34;服务器: \u0026#34;, conn.RemoteAddr().String()) //输出服务器和IP和端口号结束 fmt.Println(\u0026#34;客户端: \u0026#34;, conn.LocalAddr().String()) } func Wirte(conn net.Conn) { line := \u0026#34;啊啊啊啊啊啊啊\u0026#34; for i := 0; i \u0026lt; 5; i++ { //发送五次一样的数据 _, err := conn.Write([]byte(line)) if err != nil { fmt.Printf(\u0026#34;conn.Write error: %v\\n\u0026#34;, err) return } } } 客户端\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;net\u0026#34; ) func main() { listener, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8808\u0026#34;) if err != nil { fmt.Printf(\u0026#34;net.Listen error: %v\u0026#34;, err) return } fmt.Println(\u0026#34;服务器已开启...\u0026#34;) defer listener.Close() //设置延迟关闭,防止服务器关闭 for { //设置阻塞持续监听 conn, err := listener.Accept() go get(conn) if err != nil { fmt.Printf(\u0026#34;listener.Accept error: %v\u0026#34;, err) return } else { fmt.Println(\u0026#34;服务器: \u0026#34;, conn.LocalAddr().String()) } } } // 获取客户端输入 func get(conn net.Conn) { defer conn.Close() buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { if err == io.EOF { //处理EOF　客户端断开error fmt.Printf(\u0026#34;客户端(%v)已断开\u0026#34;, conn.RemoteAddr()) return } fmt.Printf(\u0026#34;conn.Read error: %v\u0026#34;, err) return } fmt.Printf(\u0026#34;接收到客户端输入: %v\u0026#34;, string(buf[:n])) } 此时的服务端输出的结果是：\n服务器已开启... 服务器: 127.0.0.1:8808 接收到客户端输入: 啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊 客户端发送五次数据，结果数据反而“粘”在了一起。\n粘包出现的原因 # 主要原因就是TCP数据传递模式是流模式，在保持长连接的时候可以进行多次的收和发。\n1.由Nagle算法造成的发送端的粘包：Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时，TCP并不立刻发送此段数据，而是等待一小段时间看看在等待期间是否还有要发送的数据，若有则会一次把这两段数据发送出去。 2.接收端接收不及时造成的接收端粘包：TCP会把接收到的数据存在自己的缓冲区中，然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来，就会造成TCP缓冲区中存放了几段数据。\n粘包的解决方法 # 解决TCP传输粘包有两种方法：\n使用边界符\n使用成熟的应用层协议\n感谢大佬写的文章 ：\nGolang处理TCP“粘包”问题 | JWang的博客 (wangbjun.site)\n方法1 # 再来说说方法2\n出现”粘包”的一个原因在于接收方不确定将要传输的数据包的大小，所以可以用边界符来解决粘包问题\n客户端\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;time\u0026#34; ) func main() { fmt.Println(\u0026#34;客户端开始...\u0026#34;) conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8808\u0026#34;) //链接服务器端口 if err != nil { fmt.Printf(\u0026#34;net.Dial error: %v\u0026#34;, err) return } defer conn.Close() fmt.Println(\u0026#34;客户端链接成功\u0026#34;) Wirte(conn) fmt.Println(\u0026#34;服务器: \u0026#34;, conn.RemoteAddr().String()) //输出服务器和IP和端口号结束 fmt.Println(\u0026#34;客户端: \u0026#34;, conn.LocalAddr().String()) } func Wirte(conn net.Conn) { line := \u0026#34;啊啊啊啊啊啊啊\\n\u0026#34; _, err := conn.Write([]byte(line)) time.Sleep(time.Second * 1) _, err = conn.Write([]byte(line)) time.Sleep(time.Second * 1) if err != nil { fmt.Printf(\u0026#34;conn.Write error: %v\\n\u0026#34;, err) return } } 服务端\npackage main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;net\u0026#34; ) func main() { listener, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8808\u0026#34;) if err != nil { fmt.Printf(\u0026#34;net.Listen error: %v\u0026#34;, err) return } fmt.Println(\u0026#34;服务器已开启...\u0026#34;) defer listener.Close() //设置延迟关闭,防止服务器关闭 for { //设置阻塞持续监听 conn, err := listener.Accept() go get(conn) if err != nil { fmt.Printf(\u0026#34;listener.Accept error: %v\u0026#34;, err) return } else { fmt.Println(\u0026#34;服务器: \u0026#34;, conn.LocalAddr().String()) } } } // 获取客户端输入 func get(conn net.Conn) { defer conn.Close() for { reader := bufio.NewReader(conn) slice, err := reader.ReadSlice(\u0026#39;\\n\u0026#39;) fmt.Printf(\u0026#34;检测到客户端输入: %s\u0026#34;, slice) if err != nil { if err == io.EOF { //处理EOF　客户端断开error fmt.Printf(\u0026#34;客户端(%v)已断开\\n\u0026#34;, conn.RemoteAddr()) return } fmt.Printf(\u0026#34;conn.Read error: %v\u0026#34;, err) return } } } 输出结果:\n![image-20221105191742090](D:\\Hexo Blog\\Blog\\source_posts\\Golang网络编程\\image-20221105191742090.png)\n方法2 # 使用HTTP、SSH这样成熟的应用层协议\nUDP编程 # UDP协议（User Datagram Protocol）中文名称是用户数据报协议，是OSI（Open System Interconnection，开放式系统互联）参考模型中一种无连接的传输层协议，不需要建立连接就能直接进行数据发送和接收，属于不可靠的、没有时序的通信，但是UDP协议的实时性比较好，通常用于视频直播相关领域。\n服务器端 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) func main() { addr := \u0026amp;net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8808} listener, err := net.ListenUDP(\u0026#34;udp\u0026#34;, addr) //创建UDP4链接,监听127.0.0.1:8808 if err != nil { fmt.Printf(\u0026#34;net.Listen err: %v\u0026#34;, err) return } //延迟关闭监听 defer listener.Close() for { var data [1024]byte //接收数据 n, addr, err := listener.ReadFromUDP(data[:]) //写入流接收数据 if err != nil { fmt.Println(\u0026#34;listener.ReadFromUDP err:\u0026#34;, err) continue } fmt.Printf(\u0026#34;接收数据:%v\\n 客户端:%v\\n 接收字节大小:%v\\n\u0026#34;, string(data[:n]), addr, n) // //发送数据 _, err = listener.WriteToUDP(data[:n], addr) //输出流输出数据 if err != nil { fmt.Println(\u0026#34; listener.WriteToUDP err:\u0026#34;, err) continue } _, err = listener.WriteToUDP([]byte(\u0026#34;Hello,UDP Client!\u0026#34;), addr) //向客户端发送数据 if err != nil { fmt.Printf(\u0026#34;listener.WriteToUDP err: %v\u0026#34;, err) return } } } 客户端 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; ) func main() { addr := \u0026amp;net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8808} socket, err := net.DialUDP(\u0026#34;udp\u0026#34;, nil, addr) if err != nil { fmt.Printf(\u0026#34;net.DialUDP err: %v\u0026#34;, err) return } defer socket.Close() line := \u0026#34;Hello,UDP Server\u0026#34; data, err := socket.Write([]byte(line)) if err != nil { fmt.Printf(\u0026#34; socket.Write err: %v\u0026#34;, err) return } fmt.Printf(\u0026#34;向UDP客户端%v,发送了 %v 字节的数据\u0026#34;, socket.RemoteAddr(), data) var get [1024]byte len, addr, err := socket.ReadFromUDP(get[:]) //获取服务端返回的数据 if err != nil { fmt.Printf(\u0026#34;socket.ReadFromUDP err: %v\u0026#34;, err) return } fmt.Printf(\u0026#34;UDP服务器:%v\\n 发送的内容为 %v\\n 接收字节大小: %v\\n\u0026#34;, addr, string(get[:len]), len) } HTTP编程 # 通过http.HandleFunc()和http.ListenAndServe()两个函数就可以轻松创建一个简单的Go web服务器\nWeb服务器工作流程 # Web服务器的工作原理可以简单地归纳为:\n客户机通过TCP/IP协议建立到服务器的TCP连接- 客户端向服务器发送HTTP协议请求包，请求服务器里的资源文档 服务器向客户机发送HTTP协议应答包，如果请求的资源包含有动态语言的内容，那么服务器会调用动态语言的解释引擎负责处理“动态内容”，并将处理得到的数据返回给客户端 客户机与服务器断开。由客户端解释HTML文档，在客户端屏幕上渲染图形结果 HTTP协议 # 超文本传输协议（Hyper Text Transfer Protocol，HTTP）是一个简单的请求-响应协议，它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。\nHTTP服务端 # 服务端基本代码\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { http.HandleFunc(\u0026#34;/\u0026#34;, hello) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) //启动一个8088端口的服务器 } func hello(writer http.ResponseWriter, request *http.Request) { str := \u0026#34;Hello,This is Golang HTTP Server!\u0026#34; fmt.Fprintf(writer, str) //Fprintf实现格式化的I/O } 然后访问localhost:8088就可以访问到建立的HTTP服务器了\nHander操作 # 获取请求的Body内容 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { http.HandleFunc(\u0026#34;/\u0026#34;, hander) http.ListenAndServe(\u0026#34;:8088\u0026#34;, nil) //设置监听端口，使用默认hander } // 这是Hander func hander(resp http.ResponseWriter, req *http.Request) { resp.Write([]byte(\u0026#34;Hello!\\n\u0026#34;)) //response返回Hellp switch req.Method { //获取请求的方法 case \u0026#34;GET\u0026#34;: data, err := io.ReadAll(req.Body) if err != nil { fmt.Printf(\u0026#34;io.ReadAll err: %v\u0026#34;, err) } resp.Write(data) //返回请求的Body resp.Write([]byte(\u0026#34;This is GET\u0026#34;)) break case \u0026#34;POST\u0026#34;: data, err := io.ReadAll(req.Body) if err != nil { fmt.Printf(\u0026#34;io.ReadAll err: %v\u0026#34;, err) } resp.Write(data) //返回请求的Body resp.Write([]byte(\u0026#34;This is POST\\n\u0026#34;)) break } } 返回结果:\nHTTP客户端 # 获取服务端返回的响应\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;net/http\u0026#34;) func main() { client := new(http.Client) req, err := http.NewRequest(\u0026#34;GET\u0026#34;, \u0026#34;http://localhost:8088\u0026#34;, nil) //设置请求方法,请求URL,输入流 if err != nil { fmt.Printf(\u0026#34;http.NewRequest err: %v\u0026#34;, err) } res, err := client.Do(req) //Client请求,获取响应 if err != nil { fmt.Printf(\u0026#34;client.Do err: %v\u0026#34;, err) } body := res.Body //去除响应的Body r, err := io.ReadAll(body) //输入流读取返回的Body if err != nil { fmt.Printf(\u0026#34;io.ReadAll err: %v\u0026#34;, err) } fmt.Println(string(r)) //输出返回的响应 } 其他 # net.Dial # Dial() 函数支持如下几种网络协议：tcp、tcp4（仅限 IPv4）、tcp6（仅限 IPv6）、udp、udp4（仅限IPv4）、udp6（仅限IPv6）、ip、ip4（仅限IPv4）、ip6（仅限IPv6）、unix、unixgram 和 unixpacket。\n在成功建立连接后，我们就可以进行数据的发送和接收，发送数据时，使用连接对象 conn 的 Write() 方法，接收数据时使用Read() 方法。\ndefer关键字 # 最后使用defer的会保存在栈顶，会被最先调用\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { fmt.Println(\u0026#34;defer begin\u0026#34;) // 将defer放入延迟调用栈 defer fmt.Println(1) defer fmt.Println(2) // 最后一个放入, 位于栈顶, 最先调用 defer fmt.Println(3) fmt.Println(\u0026#34;defer end\u0026#34;) } 其他笔记: [[Golang 操作数据库]] [[Golang Gin 框架入门]]\n","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/posts/golang%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/","section":"Posts","summary":"","title":"Golang网络编程","type":"posts"},{"content":"Gin 是 Go语言写的一个 web 框架，它具有运行速度快，分组的路由器，良好的崩溃捕获和错误处理，非常好的支持中间件和 json。\n框架介绍 # Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架，由于 httprouter，速度提高了近 40 倍。如果你需要极好的性能，使用 Gin 吧。 Golang Gin 文档：文档 | Gin Web Framework (gin-gonic.com) 环境配置 # 下载Gin包 # 首先下载安装 gin 包：\ngo get -u github.com/gin-gonic/gin\n需要注意的是Gin框架需要Golang 1.13以上版本\n有的时候会碰到因为GitHub被墙导致无法获取的情况，\n这时候需要改一下Go module 的模块代理，改成国内代理的网站，可以点击下面网址来更改模块代理\n七牛云 - Goproxy.cn\n测试搭建 # 创建main.go,输入如下代码：\npackage main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { r := gin.Default() r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.String(http.StatusOK, \u0026#34;搭建完成\u0026#34;) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 运行main.go\ngo run main.go 然后访问http://127.0.0.1:8888/\n显示在“搭建完成”，则说明Gin环境完成了搭建\n路由 (Route) # Gin 的路由支持 GET , POST , PUT , DELETE , PATCH , HEAD , OPTIONS 请求，同时还有一个 Any 函数，可以同时支持以上的所有请求。\n当然也可以使用 context.Any() 这个方法来匹配所有请求.\n无参数路由 # r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.String(http.StatusOK, \u0026#34;Hello,World!\u0026#34;) }) 可以使用 curl + 请求地址来快捷访问服务器\ncurl http://localhost.8888/ 返回结果:\nStatusCode : 200 StatusDescription : OK Content : Hello,World! RawContent : HTTP/1.1 200 OK Content-Length: 12 Content-Type: text/plain; charset=utf-8 Date: Sun, 20 Nov 2022 10:04:09 GMT Hello,World! Forms : {} Headers : {[Content-Length, 12], [Content-Type, text/plain; charset=utf-8], [Date, Sun, 20 Nov 2022 10:04:09 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 12 完整代码:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.String(http.StatusOK, \u0026#34;Hello,World!\u0026#34;) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 解析路径参数 # 有的时候我们需要动态的路由,例如 /user/:name , 通过嗲用不同的URL来串流不同的那么/ 这时候就要获取URL传入的参数和设置接受了\n动态路由 # r.GET(\u0026#34;/user/:name\u0026#34;, func(c *gin.Context) { //设置URL格式 Name := c.Param(\u0026#34;name\u0026#34;) //获取传入的Name c.String(http.StatusOK, \u0026#34;Hello,%s!\u0026#34;, Name) //输出 }) 请求服务器:\ncurl http://localhost:8888/user/TEST 输出结果:\nStatusCode : 200 StatusDescription : OK Content : Hello,TEST! RawContent : HTTP/1.1 200 OK Content-Length: 11 Content-Type: text/plain; charset=utf-8 Date: Sun, 20 Nov 2022 10:12:20 GMT Hello,TEST! Forms : {} Headers : {[Content-Length, 11], [Content-Type, text/plain; charset=utf-8], [Date, Sun, 20 Nov 2022 10:12:20 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 11 完整代码:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.GET(\u0026#34;/user/:name\u0026#34;, func(c *gin.Context) { //设置URL格式 Name := c.Param(\u0026#34;name\u0026#34;) //获取传入的Name c.String(http.StatusOK, \u0026#34;Hello,%s!\u0026#34;, Name) //输出 }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 获取Query参数 # r.GET(\u0026#34;/user\u0026#34;, func(c *gin.Context) { //设置URL格式 Name := c.Query(\u0026#34;name\u0026#34;) //获取传入的Name role := c.DefaultQuery(\u0026#34;role\u0026#34;, \u0026#34;Teacher\u0026#34;) //设置参数默认值 ,不存在参数返回默认值,存在参数就设定为参数 c.String(http.StatusOK, \u0026#34;Hello,%s - %s!\u0026#34;, Name, role) //输出 }) 请求服务器:\ncurl http://localhost:8888/user?name=Tom\u0026#34;\u0026amp;\u0026#34;role=Worker 需要注意的是, curl中必须给 \u0026amp; 加双引号,否则会被解析成运算符\n输出结果:\nStatusCode : 200 StatusDescription : OK Content : Hello,Tom - Worker! RawContent : HTTP/1.1 200 OK Content-Length: 19 Content-Type: text/plain; charset=utf-8 Date: Sun, 20 Nov 2022 10:24:07 GMT Hello,Tom - Worker! Forms : {} Headers : {[Content-Length, 19], [Content-Type, text/plain; charset=utf-8], [Date, Sun, 20 Nov 2022 10:24:07 GMT]} Images : {} InputFields : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 19 不带 role 参数请求:\ncurl http://localhost:8888/user?name=Tom 输出结果:\nStatusCode : 200 StatusDescription : OK Content : Hello,Tom - Teacher! RawContent : HTTP/1.1 200 OK Content-Length: 20 Content-Type: text/plain; charset=utf-8 Date: Sun, 20 Nov 2022 10:24:19 GMT Hello,Tom - Teacher! Forms : {} Headers : {[Content-Length, 20], [Content-Type, text/plain; charset=utf-8], [Date, Sun, 20 Nov 2022 10:24:19 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 20 这里看到设置的默认值起作用了.\n完整代码:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.GET(\u0026#34;/user\u0026#34;, func(c *gin.Context) { //设置URL格式 Name := c.Query(\u0026#34;name\u0026#34;) //获取传入的Name role := c.DefaultQuery(\u0026#34;role\u0026#34;, \u0026#34;Teacher\u0026#34;) //设置参数默认值 ,不存在参数返回默认值,存在参数就设定为参数 c.String(http.StatusOK, \u0026#34;Hello,%s - %s!\u0026#34;, Name, role) //输出 }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 获取POST参数 # r.POST(\u0026#34;/form\u0026#34;, func(c *gin.Context) { types := c.DefaultPostForm(\u0026#34;type\u0026#34;, \u0026#34;post\u0026#34;) username := c.PostForm(\u0026#34;username\u0026#34;) //获取POST参数中username 的值 password := c.PostForm(\u0026#34;password\u0026#34;) //设置默认值 c.String(http.StatusOK, fmt.Sprintf(\u0026#34;username:%s,password:%s,type:%s\u0026#34;, username, password, types)) }) 请求服务器:\nCURL -X POST -d \u0026#34;username=TEST\u0026amp;password=123123\u0026#34; \u0026#34;http://localhost:8888/form\u0026#34; 值得注意的是 这条指令在PowerShell里面会报错 报错为:\nInvoke-WebRequest : 找不到与参数名称“X”匹配的参数。 所在位置 行:1 字符: 6 + CURL -X POST -d \u0026#34;username=TEST\u0026amp;password=123123\u0026#34; \u0026#34;http://localhost:888 ... + ~~ + CategoryInfo : InvalidArgument: (:) [Invoke-WebRequest]，ParameterBindingException + FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.InvokeWebRequestCommand 需要将请求改为PowerShell认识的格式:\ncurl -Uri \u0026#39;http://localhost:8888/form\u0026#39; -Body \u0026#39;username=TEST\u0026amp;password=123123\u0026#39; -Method \u0026#39;POST\u0026#39; 或者可以试试转到CMD进行 CURL操作,或者下一个PostMan .\n输出结果:\nusername:TEST,password:123123,type:post PowerShell输出结果:\nStatusCode : 200 StatusDescription : OK Content : username:TEST,password:123123,type:post RawContent : HTTP/1.1 200 OK Content-Length: 39 Content-Type: text/plain; charset=utf-8 Date: Sun, 20 Nov 2022 12:39:09 GMT username:TEST,password:123123,type:post Forms : {} Headers : {[Content-Length, 39], [Content-Type, text/plain; charset=utf-8], [Date, Sun, 20 Nov 2022 12:39:09 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 39 完整代码:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.POST(\u0026#34;/form\u0026#34;, func(c *gin.Context) { types := c.DefaultPostForm(\u0026#34;type\u0026#34;, \u0026#34;post\u0026#34;) username := c.PostForm(\u0026#34;username\u0026#34;) //获取POST参数中username 的值 password := c.PostForm(\u0026#34;password\u0026#34;) //设置默认值 c.String(http.StatusOK, fmt.Sprintf(\u0026#34;username:%s,password:%s,type:%s\u0026#34;, username, password, types)) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } Query 和 POST 混合参数 # //Query 和 POST 混合 r.POST(\u0026#34;/post\u0026#34;, func(c *gin.Context) { id := c.Query(\u0026#34;id\u0026#34;) //获取Query传入的ID name := c.PostForm(\u0026#34;name\u0026#34;) //获取POST传入的Name c.JSON(http.StatusOK, gin.H{ //结果返回一个JSON \u0026#34;ID\u0026#34;: id, \u0026#34;Name\u0026#34;: name, }) }) 请求服务器:\nCURL -Uri \u0026#39;http://localhost:8888/post?id=001\u0026#39; -Body \u0026#39;name=TEST\u0026#39; -Method \u0026#39;POST\u0026#39; 返回结果:\nStatusCode : 200 StatusDescription : OK Content : {\u0026#34;ID\u0026#34;:\u0026#34;001\u0026#34;,\u0026#34;Name\u0026#34;:\u0026#34;TEST\u0026#34;} RawContent : HTTP/1.1 200 OK Content-Length: 26 Content-Type: application/json; charset=utf-8 Date: Mon, 21 Nov 2022 00:20:33 GMT {\u0026#34;ID\u0026#34;:\u0026#34;001\u0026#34;,\u0026#34;Name\u0026#34;:\u0026#34;TEST\u0026#34;} Forms : {} Headers : {[Content-Length, 26], [Content-Type, application/json; charset=utf-8], [Date, Mon, 21 Nov 2022 00: 20:33 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 26 完整代码:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() //Query 和 POST 混合 r.POST(\u0026#34;/post\u0026#34;, func(c *gin.Context) { id := c.Query(\u0026#34;id\u0026#34;) //获取Query传入的ID name := c.PostForm(\u0026#34;name\u0026#34;) //获取POST传入的Name c.JSON(http.StatusOK, gin.H{ //结果返回一个JSON \u0026#34;ID\u0026#34;: id, \u0026#34;Name\u0026#34;: name, }) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } Map参数 # //获取MAP参数 r.POST(\u0026#34;/map\u0026#34;, func(c *gin.Context) { name := c.QueryMap(\u0026#34;name\u0026#34;) //创建接收MAP c.JSON(http.StatusOK, gin.H{ //返回响应JSON \u0026#34;Name\u0026#34;: name, }) }) 请求服务器:\nCURL -Uri \u0026#39;http://localhost:8888/map?name[Jack]=001\u0026#39; -Method \u0026#39;POST\u0026#39; 返回响应:\nStatusCode : 200 StatusDescription : OK Content : {\u0026#34;Name\u0026#34;:{\u0026#34;Jack\u0026#34;:\u0026#34;001\u0026#34;}} RawContent : HTTP/1.1 200 OK Content-Length: 23 Content-Type: application/json; charset=utf-8 Date: Mon, 21 Nov 2022 00:35:36 GMT {\u0026#34;Name\u0026#34;:{\u0026#34;Jack\u0026#34;:\u0026#34;001\u0026#34;}} Forms : {} Headers : {[Content-Length, 23], [Content-Type, application/json; charset=utf-8], [Date, Mon, 21 Nov 2022 00: 35:36 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 23 完整代码:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() //获取MAP参数 r.POST(\u0026#34;/map\u0026#34;, func(c *gin.Context) { name := c.QueryMap(\u0026#34;name\u0026#34;) //创建接收MAP c.JSON(http.StatusOK, gin.H{ //返回响应JSON \u0026#34;Name\u0026#34;: name, }) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 重定向 # //GET请求重定向 r.GET(\u0026#34;/redirect\u0026#34;, func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, \u0026#34;https://www.baidu.com/\u0026#34;) }) 请求服务器:\nCURL \u0026#34;http://localhost:8888/redirect\u0026#34; 请求结果:\nStatusCode : 200 StatusDescription : OK Content : \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;script\u0026gt; location.replace(location.href.replace(\u0026#34;https://\u0026#34;,\u0026#34;http://\u0026#34;)); \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;noscript\u0026gt;\u0026lt;meta http-equiv=\u0026#34;refresh\u0026#34; content=\u0026#34;0;url=http://www.baidu.com/\u0026#34;\u0026gt;\u0026lt;/... RawContent : HTTP/1.1 200 OK Connection: keep-alive Accept-Ranges: bytes Content-Length: 227 Cache-Control: no-cache Content-Type: text/html Date: Mon, 21 Nov 2022 00:54:26 GMT P3P: CP=\u0026#34; OTI DSP COR IVA OUR... Forms : {} Headers : {[Connection, keep-alive], [Accept-Ranges, bytes], [Content-Length, 227], [Cache-Control, no-cache] ...} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 227 会发现我们跳转到了Baidu\n分组路由 (Grouping Routes) # 我们可以将拥有共同前缀URL的路由划分为一个分组路由\n//分组路由 G := r.Group(\u0026#34;/g\u0026#34;) G.POST(\u0026#34;/post\u0026#34;, func(c *gin.Context) { //POST 请求 name := c.PostForm(\u0026#34;name\u0026#34;) c.String(http.StatusOK, fmt.Sprintf(\u0026#34;Post Name: %v\\n\u0026#34;, name)) }) G.GET(\u0026#34;/get\u0026#34;, func(c *gin.Context) { //GET 请求 c.String(http.StatusOK, fmt.Sprintf(\u0026#34;Hello,World!\u0026#34;)) }) GET 请求服务器:\nCURL \u0026#34;http://localhost:8888/g/get\u0026#34; 响应结果:\nStatusCode : 200 StatusDescription : OK Content : Hello,World! RawContent : HTTP/1.1 200 OK Content-Length: 12 Content-Type: text/plain; charset=utf-8 Date: Mon, 21 Nov 2022 01:11:33 GMT Hello,World! Forms : {} Headers : {[Content-Length, 12], [Content-Type, text/plain; charset=utf-8], [Date, Mon, 21 Nov 2022 01:11:33 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 12 POST 请求服务器:\nCURL -Uri \u0026#34;http://localhost:8888/g/post\u0026#34; -Body \u0026#34;name=POST\u0026#34; -Method \u0026#34;POST\u0026#34; 响应结果:\nStatusCode : 200 StatusDescription : OK Content : Post Name: POST RawContent : HTTP/1.1 200 OK Content-Length: 16 Content-Type: text/plain; charset=utf-8 Date: Mon, 21 Nov 2022 01:16:06 GMT Post Name: POST Forms : {} Headers : {[Content-Length, 16], [Content-Type, text/plain; charset=utf-8], [Date, Mon, 21 Nov 2022 01:16:06 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 16 文件上传 # Golang 的 Gin框架还支持文件上传,文件上传需要 POST 方法.\n单文件上传 # //需要注意的是,GET请求和POST请求需要统一,否则无法上传文件 r.POST(\u0026#34;/\u0026#34;, func(c *gin.Context) { //接受上传的文件并且保存到本地 file, err := c.FormFile(\u0026#34;upload\u0026#34;) if err != nil { c.String(http.StatusBadRequest, \u0026#34;请求失败 Err: %s\u0026#34;, err.Error()) return } //获取文件名 fileName := file.Filename fmt.Println(\u0026#34;文件名为: \u0026#34;, fileName) //输出文件名 //将上传的文件保存带本地 err = c.SaveUploadedFile(file, fileName) if err != nil { c.String(http.StatusBadRequest, \u0026#34;文件保存失败 Err: %s\u0026#34;, err.Error()) return } c.String(http.StatusOK, \u0026#34;%s uploaded!\u0026#34;, file.Filename) }) r.LoadHTMLGlob(\u0026#34;./index.html\u0026#34;) //加上上传的HTML文件.这里也可以改成直接加载文件夹下的文件 r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.HTML(http.StatusOK, \u0026#34;index.html\u0026#34;, nil) }) HTML模板\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Upload.HTML\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026#34;/\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;upload\u0026#34; type=\u0026#34;file\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;uploadBtn\u0026#34; type=\u0026#34;submit\u0026#34; value=\u0026#34;上传\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 需要注意的是,如果你直接使用 Goland 编译的话就会报错找不到 HMTL 文件,这时候需要你找到你写的 Go 文件目录,手动编译文件,然后运行 . (比如我的Go文件名是GinFlierUpdate ,我需要这样手动编译)\ngo build GinFlierUpdate.go 完整代码\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.POST(\u0026#34;/\u0026#34;, func(c *gin.Context) { file, err := c.FormFile(\u0026#34;upload\u0026#34;) if err != nil { c.String(http.StatusBadRequest, \u0026#34;请求失败 Err: %s\u0026#34;, err.Error()) return } //获取文件名 fileName := file.Filename fmt.Println(\u0026#34;文件名为: \u0026#34;, fileName) //输出文件名 //将上传的文件保存带本地 err = c.SaveUploadedFile(file, fileName) if err != nil { c.String(http.StatusBadRequest, \u0026#34;文件保存失败 Err: %s\u0026#34;, err.Error()) return } c.String(http.StatusOK, \u0026#34;%s uploaded!\u0026#34;, file.Filename) }) r.LoadHTMLGlob(\u0026#34;./index.html\u0026#34;) r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.HTML(http.StatusOK, \u0026#34;index.html\u0026#34;, nil) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 多文件上传 # r.POST(\u0026#34;/\u0026#34;, func(c *gin.Context) { form, _ := c.MultipartForm() files, _ := form.File[\u0026#34;upload[]\u0026#34;] //获取文件数组 for _, file := range files { //遍历保存 //获取文件名 fileName := file.Filename fmt.Println(\u0026#34;文件名为: \u0026#34;, fileName) //输出文件名 //将上传的文件保存带本地 err := c.SaveUploadedFile(file, fileName) if err != nil { c.String(http.StatusBadRequest, \u0026#34;文件保存失败 Err: %s\u0026#34;, err.Error()) return } c.String(http.StatusOK, \u0026#34;%s uploaded!\\n\u0026#34;, file.Filename) } }) 由于 HTML 的 Input标签只能上传一个文件,所以我们要在标签内加上 multiple 属性来保证能上传多个文件\nHTML 模板\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Upload.HTML\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026#34;/\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; \u0026lt;input multiple name=\u0026#34;upload[]\u0026#34; type=\u0026#34;file\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;uploadBtn\u0026#34; type=\u0026#34;submit\u0026#34; value=\u0026#34;上传\u0026#34; formaction=\u0026#34;\u0026#34; \u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 完整代码\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.POST(\u0026#34;/\u0026#34;, func(c *gin.Context) { form, _ := c.MultipartForm() files, _ := form.File[\u0026#34;upload[]\u0026#34;] for _, file := range files { //获取文件名 fileName := file.Filename fmt.Println(\u0026#34;文件名为: \u0026#34;, fileName) //输出文件名 //将上传的文件保存带本地 err := c.SaveUploadedFile(file, fileName) if err != nil { c.String(http.StatusBadRequest, \u0026#34;文件保存失败 Err: %s\u0026#34;, err.Error()) return } c.String(http.StatusOK, \u0026#34;%s uploaded!\\n\u0026#34;, file.Filename) } }) r.LoadHTMLGlob(\u0026#34;./index.html\u0026#34;) r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.HTML(http.StatusOK, \u0026#34;index.html\u0026#34;, nil) }) r.Run(\u0026#34;:8888\u0026#34;) // 端口号8888 } 中间件 (Middleware) # 所谓中间件，就是连接上下级不同功能的函数或者软件，通常进行一些包裹函数的行为，为被包裹函数提供添加一些功能或行为。\nJAVA 中的 Filter() 就是起到了中间件的作用.\n在 Go 语言中，中间件 Handler 是封装另一个 http.Handler 以对请求进行预处理或后续处理的 http.Handler。它介于 Go Web 服务器与实际的处理程序之间，因此被称为“中间件”。\n使用中间件 # 中间件的使用相当简单,只要对需要中间件的路由使用 Use()方法即可\n实例\npackage main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { r := gin.Default() r.LoadHTMLGlob(\u0026#34;html/*\u0026#34;) adminGroup := r.Group(\u0026#34;/\u0026#34;) adminGroup.Use(gin.BasicAuth(gin.Accounts{ \u0026#34;admin\u0026#34;: \u0026#34;123456\u0026#34;, })) adminGroup.GET(\u0026#34;\u0026#34;, func(c *gin.Context) { c.HTML(http.StatusOK, \u0026#34;background.html\u0026#34;, nil) }) r.Run(\u0026#34;:8888\u0026#34;) } HTML\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;网站后台\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h\u0026gt;这里是网站后台\u0026lt;/h\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 访问测试\nCURL \u0026#34;http://localhost:8888/\u0026#34; 这里看到直接返回404\nCURL : 远程服务器返回错误: (401) 未经授权。 所在位置 行:1 字符: 1 + CURL \u0026#34;http://localhost:8888/\u0026#34; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]，WebExce ption + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand 这就是中间件起到作用了\n全局中间件 # 这个是在服务启动就开始注册，全局意味着所有API接口都会经过这里。Gin的中间件是通过Use方法设置的，它接收一个可变参数，所以我们同时可以设置多个中间件。\n// 1.创建路由 r := gin.Default() //默认带Logger(), Recovery()这两个内置中间件 r:= gin.New() //不带任何中间件 // 注册中间件 r.Use(MiddleWare()) gin.Default()默认使用了日志写入中间件 Logger和Recovery中间件\n这里新建一个自定义中间件middleware,并且注册成为全局中间件\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34;) func main() { r := gin.Default() r.Use(middleware) r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.Writer.WriteString(\u0026#34;GET API 执行\u0026#34;) }) r.Run(\u0026#34;:8888\u0026#34;) } func middleware(c *gin.Context) { fmt.Println(\u0026#34;Middleware执行\u0026#34;) } 运行发现请求先到达中间件,然后才到路由的\n[GIN-debug] Listening and serving HTTP on :8888 Middleware执行 [GIN] 2022/12/03 - 22:53:15 | 200 | 0s | ::1 | GET \u0026#34;/\u0026#34; 局部中间件 # 局部中间件意味着部分接口才会生效，只在局部使用\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34;) func main() { r := gin.Default() //局部中间件 r.GET(\u0026#34;/GET\u0026#34;, middleware, func(c *gin.Context) { c.Writer.WriteString(\u0026#34;GET API 执行\u0026#34;) }) r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.Writer.WriteString(\u0026#34;GET API 执行\u0026#34;) }) r.Run(\u0026#34;:8888\u0026#34;) } func middleware(c *gin.Context) { fmt.Println(\u0026#34;Middleware执行\u0026#34;) } Gin控制台输出结果:\n[GIN-debug] Listening and serving HTTP on :8888 [GIN] 2022/12/03 - 22:55:30 | 200 | 0s | ::1 | GET \u0026#34;/\u0026#34; Middleware执行 [GIN] 2022/12/03 - 22:56:25 | 200 | 369.4µs | ::1 | GET \u0026#34;/GET\u0026#34; 自定义中间件 # https://cloud.tencent.com/developer/article/1585029\n数值传递 # 可以使用gin.Context中的Set()方法来将中间件处理过的请求进行传递,而 Set()通过一个key来存储作何类型的数据，方便下一层获取。\nfunc (c *Context) Set(key string, value interface{}) 当我们在中间件中通过Set方法设置一些数值，在下一层中间件或HTTP请求处理方法中，可以使用下面列出的方法通过key获取对应数据。\n其中，gin.Context的Get方法返回interface{}，通过返回exists可以判断key是否存在。\nfunc (c *Context) Get(key string) (value interface{}, exists bool) 当我们确定通过Set方法设置对应数据类型的值时，可以使用下面方法获取应数据类型的值。\nfunc (c *Context) GetBool(key string) (b bool) func (c *Context) GetDuration(key string) (d time.Duration) func (c *Context) GetFloat64(key string) (f64 float64) func (c *Context) GetInt(key string) (i int) func (c *Context) GetInt64(key string) (i64 int64) func (c *Context) GetString(key string) (s string) func (c *Context) GetStringMap(key string) (sm map[string]interface{}) func (c *Context) GetStringMapString(key string) (sms map[string]string) func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) func (c *Context) GetStringSlice(key string) (ss []string) func (c *Context) GetTime(key string) (t time.Time) 示例:\npackage main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;strconv\u0026#34;) func main() { r := gin.Default() r.GET(\u0026#34;/\u0026#34;, mw, func(c *gin.Context) { Num := c.GetInt(\u0026#34;Num\u0026#34;) c.Writer.WriteString(strconv.Itoa(Num)) }) r.Run(\u0026#34;:8088\u0026#34;) } func mw(c *gin.Context) { c.Set(\u0026#34;Num\u0026#34;, 100) } 访问服务器\nCURL http://localhost:8088/ 返回响应\nStatusCode : 200 StatusDescription : OK Content : 100 RawContent : HTTP/1.1 200 OK Content-Length: 3 Content-Type: text/plain; charset=utf-8 Date: Sat, 10 Dec 2022 10:29:42 GMT 100 Forms : {} Headers : {[Content-Length, 3], [Content-Type, text/plain; charset=utf-8], [Date, Sat, 10 Dec 2022 10:29:42 G MT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 3 拦截请求与后置拦截 # 拦截请求 # 当用户请求不合法时，可以使用下面列出的gin.Context的几个方法中断用户请求：\nfunc (c *Context) Abort() func (c *Context) AbortWithError(code int, err error) *Error func (c *Context) AbortWithStatus(code int) 使用AbortWithStatusJSON()方法，中断用户请求后，则可以返回json格式的数据.\nfunc (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) 后置拦截 # 当然也可以在中间件处理完请求之后,进行拦截,这时候就要使用 gin.Context 中的 Next() 方法来实现后置拦截.\nNext 函数会挂起当前所在的函数，然后调用后面的中间件，待后面中间件执行完毕后，再接着执行当前函数。\nNext 函数定义如下:\nfunc (c *Context) Next() 示例\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34;) func main() { router := gin.New() mid1 := func(c *gin.Context) { fmt.Println(\u0026#34;mid1 start\u0026#34;) c.Next() fmt.Println(\u0026#34;mid1 end\u0026#34;) } mid2 := func(c *gin.Context) { fmt.Println(\u0026#34;mid2 start\u0026#34;) fmt.Println(\u0026#34;mid2 end\u0026#34;) } mid3 := func(c *gin.Context) { fmt.Println(\u0026#34;mid3 start\u0026#34;) fmt.Println(\u0026#34;mid3 end\u0026#34;) } router.Use(mid1, mid2, mid3) router.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { fmt.Println(\u0026#34;process get request\u0026#34;) c.JSON(http.StatusOK, \u0026#34;hello\u0026#34;) }) router.Run(\u0026#34;:8088\u0026#34;) } 请求\nCURL http://localhost:8088/ 响应\nStatusCode : 200 StatusDescription : OK Content : \u0026#34;hello\u0026#34; RawContent : HTTP/1.1 200 OK Content-Length: 7 Content-Type: application/json; charset=utf-8 Date: Sat, 10 Dec 2022 10:46:06 GMT \u0026#34;hello\u0026#34; Forms : {} Headers : {[Content-Length, 7], [Content-Type, application/json; charset=utf-8], [Date, Sat, 10 Dec 2022 10:4 6:06 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 7 控制台输出\n[GIN-debug] Listening and serving HTTP on :8088 mid1 start mid2 start mid2 end mid3 start mid3 end process get request mid1 end 异常处理 # ","date":"2022 年 11 月 25 日","externalUrl":null,"permalink":"/posts/golang-gin-%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/","section":"Posts","summary":"","title":"Golang Gin框架入门","type":"posts"},{"content":"","date":"2022 年 11 月 10 日","externalUrl":null,"permalink":"/tags/hexo/","section":"Tags","summary":"","title":"Hexo","type":"tags"},{"content":"介于Github Page访问实在蛋疼,就在Gitee上实名认证整了个Gitee Page, 本想着一台电脑同时可以使用Gitee和Github,对此颇为兴奋,结果在我Hexo d推送的时候,却遇到了个令人费解的问题.推送的时候遇到了无法推送的问题.\n并且显示Please tell me who you are.可是我平时在github上Clone和提交代码,只在Gitee上搭建博客,而我就像用一个Git完成我的需求,\n所以我就开始折腾的一下子.\n首先参考这篇文章,建立一下基本配置\n清除git的全局设置 # 建议在 git bash 中完成\n如果是之前没设置过的，就不用清除了。\n可以通过git config --global --list来查看是否设置过。\ngit config --global --unset user.name \u0026quot;你的名字\u0026quot; git config --global --unset user.email \u0026quot;你的邮箱\u0026quot;\n生成新的 SSH keys # Github\nssh-keygen -t rsa -f ~/.ssh/id_rsa.github -C \u0026quot;邮箱\u0026quot;\nGitee\n值得注意的是,一定要把github的邮箱和gitee的邮箱区别开来,即使用不同的邮箱,否则会冲突.\nssh-keygen -t rsa -f ~/.ssh/id_rsa.gitee -C \u0026quot;邮箱\u0026quot;\n然后回车就行.\n完成以后在~/.ssh / 下就可以看到生成的文件.\n识别 SSH 新的私钥 # 默认只读取 id_rsa，为了让 SSH 识别新的私钥，需要将新的私钥加入到 SSH agent 中\nssh-agent bash ssh-add ~/.ssh/id_rsa.github ssh-add ~/.ssh/id_rsa.gitee 多账号配置config # 首先\ntouch ~/.ssh/config 来创建config文件\n然后打开config 在里面填写\n#Default gitHub user Self Host github.com HostName github.com User git IdentityFile ~/.ssh/id_rsa.github # gitee Host gitee.com Port 22 HostName gitee.com User git IdentityFile ~/.ssh/id_rsa.gitee 然后添加SSH公钥\n测试 # 输入\nssh -T git@gitee.com ssh -T git@github.com 进行测试\n显示Sucess说明配置完成.\n然后打开博客的.deploy_git目录,改一下项目配置\n输入\ngit config user.name \u0026quot;名字\u0026quot;\ngit config user.email \u0026quot;邮箱\u0026quot;\n来配置项目的邮箱和名字,这样就可以一边使用Github Clone代码,一边Hexo d上传推送了.\n","date":"2022 年 11 月 10 日","externalUrl":null,"permalink":"/posts/hexo%E6%8A%98%E8%85%BE%E7%AC%94%E8%AE%B0-git%E5%88%86%E5%88%AB%E9%85%8D%E7%BD%AEgithub%E5%92%8Cgitee/","section":"Posts","summary":"","title":"Hexo折腾笔记-git分别配置Github和Gitee","type":"posts"},{"content":" 等待唤醒机制和匿名内部类 # 等待唤醒机制 # synchronized关键字 # 使用synchronized来实现并发\nclass DST{ //判断条件DST boolean A = false ; //判断条件 } class Thread_A extends Thread{ private DST dst ; public Thread_A(DST dst){ this.dst = dst; } @Override public void run() { synchronized (dst) { if (dst.A == false) { for (int Time = 0; Time \u0026lt; 5; Time++) { System.out.println(\u0026#34;Thread_A: \u0026#34; + Time); } } dst.A = true; System.out.println(\u0026#34;dst.A = true;\u0026#34;); } } } class Thread_B extends Thread{ private DST dst ; public Thread_B(DST dst){ this.dst = dst; } @Override public void run() { synchronized (dst) { if(dst.A == true) { for (int Time = 0; Time \u0026lt; 5; Time++) { System.out.println(\u0026#34;Thread_B: \u0026#34; + Time); } } } } } public class DemoStudyThread { public static void main(String[] args) throws InterruptedException { DST dst = new DST(); Thread_A thread_a = new Thread_A(dst);//同步DST Thread_B thread_b = new Thread_B(dst); thread_a.start(); thread_b.start(); } } 输出结果:\nThread_A: 0 Thread_A: 1 Thread_A: 2 Thread_A: 3 Thread_A: 4 dst.A = true; Thread_B: 0 Thread_B: 1 Thread_B: 2 Thread_B: 3 Thread_B: 4 wait()和notify() # wait:线程不再活动，进入wait set中，不会浪费cpu,这时的状态就是waiting,必须等着另一个线程执行唤醒notify动作，把在waiting中的线程唤醒，这时waiting 中的线程从waiting set中释放出来。 notify：从waiting set中选取一个线程释放， notifyAll:把waiting set中所有的线程唤醒。 注意事项：从waiting set中释放的线程并不代表可以立即恢复执行，只是进入可执行状态；\nclass DST{ //判断条件DST boolean A = false ; //判断条件 } class Thread_A extends Thread{ private DST dst ; public Thread_A(DST dst){ this.dst = dst; } @Override public void run() { synchronized (dst) { if (dst.A == false) { System.out.println(\u0026#34;Thread_A\u0026#34;); } dst.A = true; } } } class Thread_B extends Thread{ private DST dst ; public Thread_B(DST dst){ this.dst = dst; } @Override public void run() { synchronized (dst) { if (dst.A == false) { //在这里如果判断条件是false,线程B则会进入休眠 try { System.out.println(\u0026#34;dst.wait();\u0026#34;); dst.wait();//如果为false,线程B则会休眠，后面的代码（测试代码A）将不会执行 System.out.println(\u0026#34;Thread_B(Tst_A)\u0026#34;);//测试代码A } catch (InterruptedException e) { throw new RuntimeException(e); } }else { dst.notify(); System.out.println(\u0026#34;dst.notify();\u0026#34;); } } } } public class DemoStudyThread { public static void main(String[] args) throws InterruptedException { DST dst = new DST(); Thread_A thread_a = new Thread_A(dst);//同步DST Thread_B thread_b = new Thread_B(dst); thread_a.start(); thread_b.start(); } } 输出结果:\nThread_A dst.notify(); 注意事项：\nwait和notify方法必须要用同一个锁对象,锁对象只能唤醒同一个锁对象中等待的线程。 锁对象可以是任意对象。 wait和notify方法必须要放在同步代码块或者同步方法中。 mbda创建匿名内部类\nLambda标准格式 # 由3部分组成，参数、箭头、代码。（参数类型 参数名称）-\u0026gt;{代码语句}\n利用匿名内部类创建线程 # 好处：可以省去实现类的定义，即不用再单独定义一个Runnable的实现类了。\npublic class DemoTest { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int Time = 0; Time \u0026lt; 3; Time ++) { System.out.println(\u0026#34;New Thread running: \u0026#34; + Time ); } } }).start(); } } 输出结果:\nNew Thread running: 0 New Thread running: 1 New Thread running: 2 ","date":"2022 年 09 月 11 日","externalUrl":null,"permalink":"/posts/java%E7%BC%96%E7%A8%8B%E8%BF%9B%E9%98%B63/","section":"Posts","summary":"","title":"JAVA编程进阶(3)","type":"posts"},{"content":"","date":"2022 年 09 月 11 日","externalUrl":null,"permalink":"/tags/java/","section":"Tags","summary":"","title":"JAVA","type":"tags"},{"content":" 线程的定义以及创建 # 多线程 # 进程：一个内存中正在运行的应用程序\n线程：是最小的执行单元，一个进程中可以有多个线程。\n并发：多个事件在同一时间段内发生。\n并行：多个事件在同一时刻发生。\n在这之前，接触的都是单线程，即一个线程结束后才开始另一个线程的执行；多个线程同时执行任务，叫多线程。\n线程的创建 # 创建线程需要继承Thread类\n定义Thread类的子类，并且重写该类的run()方法，该run()的方法体就代表了线程需要完成的任务，因此run()方法也称为线程执行体。\n创建Thread子类的类对象，即线程对象\n调用线程对象的start()方法来启动线程。\nThread类常用方法 # public Thread(); public Thread(String name); public String getName();获取当前线程的名称。 public void start();启动线程，开始执行线程中的run方法 public void run();线程要执行的任务在此处定义 public static Thread currentThread();返回正在执行的线程对象的引用。 public class Demo2 extends Thread { public static void main(String[] args) { Demo2 demo2 = new Demo2(); //实现线程 demo2.start(); //start()调用启动线程 } public void run(){ //重写线程run方法 for(int Time = 0; Time \u0026lt;= 20; Time ++) { System.out.print(Time + \u0026#34; \u0026#34;); } } } 输出结果:\n线程的创建，实现Runnable接口 # 因为在java中只支持单继承，一个类如果继承了Thread类，就没办法继承别的类，因此可以通过去实现一个接口，创建线程对象，这样该类就可以去继承别的类了。\n定义Runnable接口的实现类，并重写该接口的run()方法，该run()的方法体就代表了线程需要完成的任务，因此run()方法也称为线程执行体。\n创建Runnable实现类的类对象，并以此类对象作为Thread的参数来创建Thread对象，该对象才是真正的线程对象。\n3.调用线程对象的start()方法启动线程。\nclass TestThread implements Runnable{ //实现Runnable @Override public void run() { for(int Time = 0; Time \u0026lt;= 10; Time ++) { System.out.print(Time + \u0026#34; \u0026#34;); } } } public class Demo2 { //运行测试 public static void main(String[] args) { TestThread testThread = new TestThread(); Thread thread = new Thread(testThread); thread.start(); } } 输出结果:\n","date":"2022 年 09 月 11 日","externalUrl":null,"permalink":"/posts/java%E7%BC%96%E7%A8%8B%E8%BF%9B%E9%98%B62/","section":"Posts","summary":"","title":"JAVA编程进阶(2)","type":"posts"},{"content":" 其他类 # Calendar类 # java.util.Calendar类是一个抽象类，是java日期处理的核心类之一。它提供了在特定时刻和一组日历字段之间进行转换的方法例如YEAR，MONTH，DAY_OF_MONTH，HOUR等，以及操作日历字段等功能。\n常用方法 # public static Calendar getInstance(); 获得一个日历\npublic int get(int field);返回给定日历字段的值。\npublic void set(int filed,int value);将给定的日历字段设置为给定值。\npublic Date getTime();返回是一个calendar的时间值，\npublic abstract void add(int field,int amount);为给定的日历字段添加或减去指定的时间量，amount为正数，代表添加，负数代表减去。\npublic Date getTime();返回一个当前日历的时间值。\nimport java.util.Calendar; import java.util.Date; public class Study { public static void main(String[] args) { Calendar calendar = Calendar.getInstance();//获得一个日历 System.out.println(calendar.get(Calendar.YEAR));//调用get方法获取某一字段的值 calendar.add(Calendar.YEAR,1000);//年份加1000 Date date = calendar.getTime();//返回时间 System.out.println(date); } } 输出结果: System类 # System 类位于 java.lang 包，代表当前 Java 程序的运行平台，系统级的很多属性和控制方法都放置在该类的内部。由于该类的构造方法是 private 的，所以无法创建该类的对象，也就是无法实例化该类。\n常用方法\npublic static long currentTimeMillis();返回以毫秒为单位的当前时间。\npublic static void arraycopy(object src,int srcPos,object dest,int destPos,int length);\nStringBuilder类 # 由于String类的对象是不能更改内容的，所以引入可以更改字符串内容的StringBuider类对象。\npublic StringBuilder():空的构造方法。\npublic StringBuilder(String str);带参数的构造方法，将字符串添加进去，与普通的String类无太大区别。\npublic stringBuilder append();添加任意类型的字符串形式。返回当前对象本身。有多种重载方式。\ntoString方法，将StringBuilder类转化为Sting类\n封装类 # 像int、float、char等基本数据类型是不能像引用数据类型那样调用方法的。\n装箱与拆箱 # 装箱：从基本数据类型转换为对应的应用数据类型。\n拆箱：从引用数据类型转换为基本数据类型。\n以Integer和Int类型为例\n基本数据类型-\u0026gt;引用数据类型 # 引用数据类型-\u0026gt;基本数据类型 # 基本数据类型与字符串之间的转换 # 基本数据类型-\u0026gt;字符串 # 将基本数据类型+\u0026quot;\u0026quot;;\n字符串-\u0026gt;基本数据类型 # public static byte parseByte(String s);将字符串转换成byte基本类型。 public static byte parseShort(String s);将字符串转换成short基本类型。 public static byte parseInt(String s);将字符串转换成int基本类型。 public static byte parseLong(String s);将字符串转换成long基本类型。 public static byte parseFloat(String s);将字符串转换成flaot基本类型。 public static byte parseDouble(String s);字符串转换成double基本类型。 public static byte parseBoolean(String s);字符串转换成boolean基本类型。 注意事项：如果字符串内容无法正确转换为对应的基本数据类型，则会抛出异常。\n","date":"2022 年 09 月 07 日","externalUrl":null,"permalink":"/posts/java%E7%BC%96%E7%A8%8B%E8%BF%9B%E9%98%B61/","section":"Posts","summary":"","title":"JAVA编程进阶(1)","type":"posts"},{"content":"此文章是派蒙Bot(PaimengBot)的一些问题总结和处理方法，仅供参考\n项目地址 ： https://github.com/RicheyJang/PaimengBot\n开局爆红（Go.mod） # 注意看报错显示:没有go.mod文件。所以就需要到gihub下载源代码，在那个项目里面编写\n派蒙Bot Github地址 ：https://github.com/RicheyJang/PaimengBot\n点击code,点击Download ZIP下载源代码。解压到想要放到的位置上。\n在plugins文件夹下建立插件名称为题的文件夹（比如我的就是plugins\\HiOSU）\n然后将PaimengBot文件夹拖到Goland，创建项目。\n一直更新Go模块依赖 # 当我们把项目整好以后，打开会发现有这样的提示:\n经常等半天结果报错了，\n这时候要切换GOPROXY国外转到国内。\n打开PowerShell输入\n#打开Go Mod 功能 export GO111MODULE=auto # 配置 GOPROXY 环境变量为国内代理 export GOPROXY=https://goproxy.cn,direct 或者是\nwindows + R 输入 cmd ,然后输入这个：\n#打开Go Mod 功能 go env -w GO111MODULE=auto #将 GOPROXY 换为国内代理 go env -w GOPROXY=https://goproxy.cn,direct 接着输入 go env,显示GO111MODULE=auto 和 GOPROXY=https://goproxy.cn,direct就算配置成功了\n如何测试 # 编写好代码后，需要测试有没有Bug，但是不知道咋测试。\n代码写好了，也在main.go中引用了，如何测试下能否运行呢，这里要用到Go语言中的Go Build命令来编译。\ngo-cqhttp环境 # 先去下在go - cqhttp： https://github.com/Mrs4s/go-cqhttp/releases，下载对应的系统，解压打开，像配置PaimengBot一样配置好cqhttp。\n编译测试 # windows + R 输入 cmd ,cd 到你项目的cmd文件夹下，\n或者找到项目文件夹下的cmd文件夹在路径处输入 cmd\n就可以转过去了。\n然后输入\ngo build main.go 将 mian.go编译成为可执行文件。\n然后按照运行PaimengBot的方法运行就可以了\n测试结果:\n整了个复读的插件作为测试，输入\u0026quot;RPGchat\u0026quot;，返回\u0026quot;RPGchat\u0026quot;\n如图可见，测试成功。开发环境到此配置完成。\n接下来，就可以开始愉快的开发了♪(๑ᴖ◡ᴖ๑)♪\n其他 # Golang 交叉编译 (跨平台编译)\n相关笔记: [[Golang网络编程]]\n","date":"2022 年 06 月 05 日","externalUrl":null,"permalink":"/posts/%E6%B4%BE%E8%92%99bot%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/","section":"Posts","summary":"","title":"派蒙bot开发环境配置","type":"posts"},{"content":"其他相关笔记: [[派蒙bot开发环境配置]] [[Golang 常用标准库]]\n什么是Go语言 # Go 语言被设计成一门应用于搭载 Web 服务器，存储集群或类似用途的巨型中央服务器的系统编程语言。\n环境安装 # 可以参考这个教程 ：Go 语言环境安装 菜鸟教程 (runoob.com)\nGo语言的基础构成 # 包声明 引入包 函数 变量 语句 \u0026amp; 表达式 注释 package main //包声明 import \u0026#34;fmt\u0026#34; //引入包 func main() { //main函数 // 可执行程序必须包含的函数 //单行注释 /*多行注释*/ var A int = 10 //声明变量 fmt.Println(A) //Go中声明变量必须使用 fmt.Println(\u0026#34;Hello, World!\u0026#34;) //语句 } Hello,World! # 使用Go语言输出 Hello,World!\npackage main //包声明 main ,任何可执行的Go程序必须的包 import \u0026#34;fmt\u0026#34; //引入fmt包 func main() { //main函数 ,每个可执行程序必须包含 //单行注释 /*多行注释*/ /* 这是我的第一个简单的程序 */ fmt.Println(\u0026#34;Hello, World!\u0026#34;) //语句 } Go语言基础语法 # Go 标记 # Go 程序可以由多个标记组成，可以是关键字，标识符，常量，字符串，符号。\n比如 fmt.Println(\u0026quot;Hello, World!\u0026quot;) ,fmt , println ,\u0026quot;Hello,World!\u0026quot; 和 括号以及点都是Go标记。\n行分隔符 # 在Go程序中，一行代表一行语句的结束，每个语句不需要像C，JAVA一样加分号 ; 结尾。\nfmt.Println(\u0026#34;Hello, World!\u0026#34;) fmt.Print(\u0026#34;Hello,Go!\u0026#34;) package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, World!\u0026#34;) fmt.Print(\u0026#34;Hello,Go!\u0026#34;) } 输出结果:\n变量的声明 # Go语言中声明变量必须使用 var 关键字，而且声明的变量必须使用。\n有两种方式来声明变量，\n1.指定类型变量的声明\n格式 ： var 变量名 变量类型\n此方式声明是有默认值的\n2,根据值自判断的变量声明\n格式 ： var 变量名 = 值\n此方法声明，必须赋值\npackage main import \u0026#34;fmt\u0026#34; func main() { var A string = \u0026#34;123\u0026#34; //指定类型的变量 var B string= \u0026#34;987\u0026#34; //指定类型为字符串的名为 B的变量 var C = 100 //自判断变量的声明 var D = true fmt.Println(A) //输出 fmt.Println(B) fmt.Println(C) fmt.Println(D) } 输出结果 ：\n包的引入 # Go语言引入包需要使用 import 关键字，引用格式为 import \u0026quot; 包名 \u0026quot; ,调用格式为 包名.方法名() 或 包名.变量名 .引入的包和变量一样，一经引用，必须使用。\npackage main //包声明 main ,任何Go程序必须的包 import \u0026#34;fmt\u0026#34; //引入fmt包 import \u0026#34;math\u0026#34; //引入math包 func main() { //main函数 ,每个可执行程序必须包含 fmt.Print(math.Pi) //调用math包的变量Pi } 注意： 一个可执行程序有且仅有一个 main 包。\n在Go语言中也可以使用 fmt.Sprint 来格式化字符串\npackage main import \u0026#34;fmt\u0026#34; func main() { var A string = \u0026#34;123\u0026#34; var B string= \u0026#34;987\u0026#34; // fmt.Sprint格式化字符串，并赋值给新字符串 var C string = fmt.Sprint(A,B) fmt.Print(C) } 输出结果为 ：\n注释 # 注释不会被编译，每一个包应该有相关注释。\n单行注释是最常见的注释形式， // 开头是单行注释。多行注释也叫块注释，以 /* 开头，并以 */ 结尾。\n//单行注释 /*多行 注释*/ 标识符 # 标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(AZ和az)数字(0~9)、下划线_组成的序列，但是第一个字符必须是字母或下划线而不能是数字。\n数据类型 # 布尔类型 # 值只有 ture 和 false ，声明方式 var B bool ，默认值为 false\n和 JAVA 不同的是，JAVA使用 Boolean 关键字来表示布尔值 ，Go 语言用 bool 来表示，注意区分\n数字类型 # 整数类型 # 整数 int 类型，有正负之分。\nint8 //有符号 8 位整型,取值 -128 到 127 int16 //有符号 16 位整型,取值 -32768 到 32767 int32 //有符号 32 位整型,取值 -2147483648 到 2147483647 int64 //有符号 64 位整型,取值 -9223372036854775808 到 9223372036854775807 无符号整数 int 前面加个u (unsigned) ,变成无符号整数 uint (unsigned integer),无符号整数一定是正整数。\n和有符号整数一样，也是有各种位数的\nuint8 //无符号 8 位整型,取值 0 到 255 uint16 //无符号 16 位整型,取值 0 到 65535 uint32 //无符号 32 位整型,取值 0 到 4294967295 uint64 //无符号 64 位整型,取值 0 到 18446744073709551615 浮点型 # float32 和 float64 IEEE754标准\nGo语言的浮点型没有float ,有 float32 和 float64 ，无 double 类型\nfloat32 //32位浮点型数 float64 //64位浮点型数 字符串类型 # string ,一串固定长度的字符连接起来的字符序列。\n使用双引号 \u0026quot; \u0026quot;，或者反引号` `来创建。\n在JAVA中 ，字符串类型是 String 第一个字母是大写，Go语言中，第一个字母小写\npackage main import \u0026#34;fmt\u0026#34; func main() { var A string = \u0026#34;A\u0026#34; //定义字符串 var B string = `B` fmt.Println(\u0026#34;var A = \u0026#34; , A ) //输出 fmt.Println(`var B = ` , B ) } 输出结果 ：\n合并字符串\n使用加号来合并字符串\npackage main import \u0026#34;fmt\u0026#34; func main() { var A string = \u0026#34;A\u0026#34; var B string = `B` var str string = A + B fmt.Println(\u0026#34;var A = \u0026#34; , A ) fmt.Println(`var B = ` , B ) fmt.Println(\u0026#34;var str1 = \u0026#34; , str ) } 输出结果:\nfmt包中的 Sprintf来合并字符串\n格式 : fmt.Sprintf(格式化样式, 参数列表)\n**格式化样式：**字符串形式，格式化符号以 % 开头， %s 字符串格式，%d 十进制的整数格式。 **参数列表：**多个参数以逗号分隔，个数必须与格式化样式中的个数一一对应，否则运行时会报错。 package main import \u0026#34;fmt\u0026#34; func main() { var A = \u0026#34;123\u0026#34; var B = \u0026#34;123\u0026#34; //fmt包中的Sprint方法格式化输出 var str = fmt.Sprintf(\u0026#34;A = %s , B = %s\u0026#34;,A , B) fmt.Println( \u0026#34;var str :\u0026#34; ,str ) } 输出结果:\n派生类型 # 指针类型（Pointer） 数组类型 (Arrayllist) 结构化类型(struct) 通道类型 (Channel) 函数类型(Fanc) 切片类型 接口类型（interface） Map 类型 相关笔记: [[Golang派生类型]] 注意 # Go语言中在不同数据类型之间赋值时需要显式的转换，不像其他如php,java可自动转换。\n比如下面这个例子:\n会出现类型不匹配的报错。\n需要强制转换一下:\npackage main import \u0026#34;fmt\u0026#34; func main() { var A int8 = 10 var B float32 = 0.5 var C = float32(A) + B //将 int强制转换为 float32 fmt.Println(C) } 输出结果:\n而在JAVA中不会出现这个问题。\n变量 # 声明变量 # 在Go语言中,使用var关键字声明变量，且变量一旦声明，必须使用。\n这里和JAVA中不同，需要注意。\n声明格式\nvar Name Type //声明变量的格式 var ： 变量声明关键字\nName : 变量名字\nType : 变量类型\n//声明变量 package main import \u0026#34;fmt\u0026#34; func main() { var A string = \u0026#34;Hello,Go\u0026#34; //声明名为 A 的整数变量 fmt.Println(A) //输出 } 输出结果：\n批量声明变量 # 在Go中，可以使用 var + ()来实现批量声明变量的操作。\n声明格式：\n//批量声明变量 var ( Name string Sex string Age int ) 类型推断 # 在Go中有一种很方便的声明变量或者常量的方式，就是类型推断\n声明格式：\nvar Name = value var : 变量声明关键字\nName : 变量名\nvalue : 初始化值\n//类型推断 package main import \u0026#34;fmt\u0026#34; func main() { var Name = \u0026#34;たかみ ちか\u0026#34; fmt.Println(Name) //输出 } 输出结果:\n变量的初始化 # Go语言中声明变量，会自动进行初始化操作，并且赋默认值。\nint类型默认值为0,string类型默认值为\u0026quot;\u0026quot;，bool类型默认值为false\n//变量初始化 var ( Name string = \u0026#34;Takami Chika\u0026#34; Sex string = \u0026#34;female\u0026#34; Age int = 16 ) 批量初始化 # 一次性可以初始化多个变量，中间用逗号隔开变量和值\n//批量初始化 var A ,B , C = 1, 2, 3 短变量声明 # 在函数内部，可以使用短变量来更为快速地声明变量。\n//短变量声明 package main import \u0026#34;fmt\u0026#34; func main() { Name := \u0026#34;短变量\u0026#34; //短变量声明 fmt.Println(Name) //输出 } 短变量只能在函数内部声明，无法在函数外部声明\n匿名变量 # 如果接收到多个变量，而有一些变量暂时使用不到，可以使用下划线 _ 来声明匿名变量来代替变量名称。\n匿名变量声明后，传入的任何值将会被抛弃，且不能在后续的代码中使用。\nvar _ = \u0026#34;\u0026#34; var Name,_ = \u0026#34;KSM\u0026#34; , 123 常量 # 在编译时就确定的值，运行后无法改变。\n声明常量 # 在Go语言中，使用 const 关键字来定义常量\nconst Name Type = value const : 常量定义关键字\nName : 常量名\nType : 常量数据类型 , 因为有 类型推断，也可以不用写数据类型\nvalue ：常量值\nGo中的常量，在定义时就必须要赋初值，且后续无法修改。（比JAVA方便了不少）\nJAVA 中的常量 : private static final int A = 100;\nGo 中的常量 : const A = 100\n//常量 package main import \u0026#34;fmt\u0026#34; func main() { const Name string = \u0026#34;CYaRon!\u0026#34; fmt.Println(Name) } 输出结果:\n当然，常量也是可以批量定义和初始化的\npackage main import \u0026#34;fmt\u0026#34; func main() { const Name string = \u0026#34;CYaRon!\u0026#34; fmt.Println(Name) const ( A = \u0026#34;A\u0026#34; B = 10 ) const C, D, E = 1,2,3 } IOTA # 关键字iota # iota可以理解为一个可以被编译器修改的变量，初始值为0，调用一次增加1，遇到下一个const关键字重置为0。\npackage main import \u0026#34;fmt\u0026#34; func main() { const ( A = iota //0 B = iota //类似 JAVA 中的 i++ C = iota D = iota E = iota //4 ) fmt.Println(\u0026#34;const A = \u0026#34; ,A) fmt.Println(\u0026#34;const E =\u0026#34;,E) const ( //iota被重置 F = iota //0 G= iota //1 ) fmt.Println(\u0026#34;const F = \u0026#34;,F) fmt.Println(\u0026#34;const G = \u0026#34;,G) } 输出结果:\n使用 _ (下划线)来跳过某些值 # package main import \u0026#34;fmt\u0026#34; func main() { const ( A = iota //0 B = iota //1 _ //2 D = iota //3 E = iota //4 ) fmt.Println(\u0026#34;const A = \u0026#34; ,A) fmt.Println(\u0026#34;const E =\u0026#34;,E) } 运行结果:\niota中间插队 # 在iota中间，如果被别的常量打断，iota 会被覆盖掉，不再继续自增。但是用另一个 iota 接一下，又会继续自增。\n被打断有iota接 package main import \u0026#34;fmt\u0026#34; func main() { const ( A = iota //0 B = iota //1 C = 100 D = iota //3 E = 100 F = iota //5 G //6 ) fmt.Println(\u0026#34;const A = \u0026#34; , A ) fmt.Println(\u0026#34;const D = \u0026#34; , D ) fmt.Println(\u0026#34;const G = \u0026#34; , G ) } 输出结果：\n被打断没有iota来接 package main import \u0026#34;fmt\u0026#34; func main() { const ( A = iota //0 B = iota //1 C = 100 D = iota //3 E = 100 F //100 G //100 ) fmt.Println(\u0026#34;const A = \u0026#34; , A ) fmt.Println(\u0026#34;const D = \u0026#34; , D ) fmt.Println(\u0026#34;const E = \u0026#34; , E ) fmt.Println(\u0026#34;const G = \u0026#34; , G ) } 输出结果:\n运算符 # 算数运算符 # 符号 描述 + 相加 - 相减 * 相乘 / 相处 % 取余 在Go语言中， 自增 ( ++ ) 和 自减 ( -- ) 算单独的语句，不属于运算符。\npackage main import \u0026#34;fmt\u0026#34; func main() { var A = 100 var B = 10 fmt.Println(\u0026#34;A+B : \u0026#34; ,A + B) fmt.Println(\u0026#34;A-B : \u0026#34; ,A - B) fmt.Println(\u0026#34;A*B : \u0026#34; ,A * B) fmt.Println(\u0026#34;A/B : \u0026#34; ,A / B) fmt.Println(\u0026#34;A%B : \u0026#34; ,A % B) } 输出结果:\n关系运算符 # 运算符 描述 == 检查两个值是否相等，如果相等返回 True, 否则返回 False。 != 检查两个值是否不相等，如果不相等返回 True, 否则返回 False。 \u0026gt; 检查左边值是否大于右边值，如果是返回 True, 否则返回 False。 \u0026gt;= 检查左边值是否大于等于右边值，如果是返回 True ,否则返回 False。 \u0026lt; 检查左边值是否小于右边值，如果是返回 True ,否则返回 False。 \u0026lt;= 检查左边值是否小于等于右边值，如果是返回 True, 否则返回 False。 package main import \u0026#34;fmt\u0026#34; func main() { var Bool bool //定义布尔值 Bool var A = 10 var B = 100 var C = 10 Bool = A==B fmt.Println(\u0026#34;A == B: \u0026#34; , Bool) // false Bool = A!=B fmt.Println(\u0026#34;A != B: \u0026#34; , Bool) //true Bool = A \u0026gt; C fmt.Println(\u0026#34;A \u0026gt; C: \u0026#34; , Bool) //false Bool = A \u0026lt; B fmt.Println(\u0026#34;A \u0026lt; B: \u0026#34; , Bool) //true Bool = A \u0026lt;= B fmt.Println(\u0026#34;A \u0026lt;= B: \u0026#34; , Bool) //true Bool = A \u0026gt;= C fmt.Println(\u0026#34;A \u0026gt;= C: \u0026#34; , Bool) //true } 输出结果:\n逻辑运算符 # 运算符 描述 \u0026amp;\u0026amp; 逻辑 AND 运算符。 如果两边的操作数都是 True，则为 True，否则为 False。 || 逻辑 OR 运算符。 如果两边的操作数有一个 True，则为 True，否则为 Fal ! 逻辑 NOT 运算符。 如果条件为 True，则为 False，否则为 True package main import \u0026#34;fmt\u0026#34; func main() { var S bool var A = true var B = false S = A \u0026amp;\u0026amp; B fmt.Println(\u0026#34;A \u0026amp;\u0026amp; B: \u0026#34; , S) S = A B fmt.Println(\u0026#34;A B: \u0026#34; , S) S = !A fmt.Println(\u0026#34;!A: \u0026#34; , S) } 输出结果:\n位运算符 # 运算符 描述 \u0026amp; 参与运算的两数各对应的二进位相与。（两位均为1才为1） | 参与运算的两数各对应的二进位相或。（两位有一个为1就为1） ^ 参与运算的两数各对应的二进位相异或，当两对应的二进位相异时，结果为1。（两位不一样则为1） \u0026laquo; 左移n位就是乘以2的n次方。“a\u0026laquo;b”是把a的各二进位全部左移b位，高位丢弃，低位补0。 \u0026raquo; 右移n位就是除以2的n次方。“a\u0026raquo;b”是把a的各二进位全部右移b位。 位运算符的概念可以参考这篇教程来更好的理解： C 运算符\npackage main import \u0026#34;fmt\u0026#34; func main() { var A = 15 //1111 var B = 10 // 1010 var C uint = 1 //1 Y := A \u0026amp; B fmt.Println(\u0026#34;A \u0026amp; B: \u0026#34; , Y) //1010 = 10 Y = A B fmt.Println(\u0026#34;A B: \u0026#34; , Y) //1111 = 5 Y = A ^ B fmt.Println(\u0026#34;A ^ B: \u0026#34; , Y) //0101 = 5 Y = A \u0026lt;\u0026lt; C fmt.Println(\u0026#34;A \u0026lt;\u0026lt; C: \u0026#34; , Y)//11110 = 30 Y = A \u0026gt;\u0026gt; C fmt.Println(\u0026#34;A \u0026gt;\u0026gt; C: \u0026#34; , Y)//0111 = 7 } 输出结果:\n赋值运算符 # 运算符 描述 = 简单的赋值运算符，将一个表达式的值赋给一个左值 += 相加后再赋值 -= 相减后再赋值 *= 相乘后再赋值 /= 相除后再赋值 %= 求余后再赋值 \u0026laquo;= 左移后赋值 \u0026raquo;= 右移后赋值 \u0026amp;= 按位与后赋值 l= 按位或后赋值 ^= 按位异或后赋值 package main import \u0026#34;fmt\u0026#34; func main() { var R = 20 fmt.Println(\u0026#34;var R = 20\u0026#34;) R += 1 fmt.Println(\u0026#34;R += 1: \u0026#34;,R) // R = R + 1 R -= 1 fmt.Println(\u0026#34;R -= 1: \u0026#34;,R) // R = R - 1 fmt.Println(\u0026#34;++++++++++++++++\u0026#34;) var A = 10 fmt.Println(\u0026#34;var A = 10\u0026#34;) A *= 2 fmt.Println(\u0026#34;A *= 2: \u0026#34;,A) // A = A * 2 A /= 4 //a = 20/4 fmt.Println(\u0026#34;A /= 4: \u0026#34;,A) // A = A / 4 A %= 10 //5%10 fmt.Println(\u0026#34;A %= 10: \u0026#34;,A)// A = A % 10 fmt.Println(\u0026#34;++++++++++++++++\u0026#34;) var G = 15 //1111 fmt.Println(\u0026#34;var G = 15\u0026#34;) G \u0026lt;\u0026lt;= 1 fmt.Println(\u0026#34;G \u0026lt;\u0026lt;= 1: \u0026#34;,G) // 1111 ---\u0026gt; 11110 , 11110 = 30 G \u0026gt;\u0026gt;= 1 fmt.Println(\u0026#34;G \u0026gt;\u0026gt;= 1: \u0026#34;,G) // 1111 ---\u0026gt; 01111 , 01111 = 15 fmt.Println(\u0026#34;++++++++++++++++\u0026#34;) var E = 10 // 1010 E \u0026amp;= 10 fmt.Println(\u0026#34;E \u0026amp;= 10: \u0026#34;,E) //1010 \u0026amp; 1010 ---\u0026gt; 1010 , 1010 = 10 E = 15 fmt.Println(\u0026#34;E = 10: \u0026#34;,E) //1010 % 1111 ---\u0026gt; 1111 , 1111 = 15 E ^= 10 fmt.Println(\u0026#34;E ^= 10: \u0026#34;,E) //1010 ^ 1010 ---\u0026gt; 0101 , 0101 = 5 } 输出结果:\n条件判断语句 # if - else 语句 # 语法格式:\nif (布尔表达式){ //布尔值为true执行花括号内语句 语句1 } else { //布尔值为false执行else内语句 语句2 } package main import \u0026#34;fmt\u0026#34; func main() { var A = 2 if (A == 10){ //2 != 10结果为false,执行else内的语句 fmt.Println(\u0026#34;A = 10\u0026#34;) }else { fmt.Println(\u0026#34;A = \u0026#34;,A) } } 输出结果:\nif嵌套语句 # 在 if 内添加if语句，实现嵌套效果.\n语法格式：\nif (布尔表达式){ 语句1 if(B == 4) { 语句1.1 } } package main import \u0026#34;fmt\u0026#34; func main() { var B = 4 if (B \u0026gt; 2){ // 4 \u0026gt; 2 执行语句1 fmt.Println(\u0026#34;B \u0026gt; 2\u0026#34;) //语句1 if(B == 4) { //B = 4 ,执行语句1.1 fmt.Println(\u0026#34;B = 4\u0026#34; ) //语句1.1 } } } 运行结果:\nif-else if-if 语句 # 单纯是if else是不够用的，所以Go语言还有else if语句。可以搭配if ，else使用\nfunc main() { var A = 10 if (A == 5){ fmt.Println(\u0026#34;A = 5\u0026#34;) }else if (A == 10) { fmt.Println(\u0026#34;A = 10\u0026#34;) }else{ fmt.Println(\u0026#34;A != 5 \u0026amp;\u0026amp; A != 10\u0026#34;) } } 输出结果:\nswitch语句 # switch用来执行不同条件的不同动作，switch包含case的分支，swicth会从上到下一次判断各个case分支。case都是唯一的，而且自带一个break效果，一旦执行一个case就不会执行后面的case，如果需要执行后面的case,需要加 fallthrough .\nfallthrough 要放在要第一个执行的case内\ndefault代表如果没有可执行的 case ，则会执行 default下的语句。\ncase中值的数据类型必须和switch中的数据类型相同, 多个值也可以写在一个case中, 但要用逗号隔开\n格式 :\nswitch Var { case var1: //语句1 case var2: //语句2 default: //语句3 } package main import \u0026#34;fmt\u0026#34; func main() { var A = 4 var B = 2 var C = A var D = 4 switch C { //判断 case A: fmt.Println(\u0026#34;A = \u0026#34; , A) fallthrough // D 和 A都为4，如果不加fallthrough只能执行A 的case case B: fmt.Println(\u0026#34;B = \u0026#34; , B) case D: fmt.Println(\u0026#34;D = \u0026#34; , D) default: fmt.Println(\u0026#34;C = \u0026#34; , C) } } 输出结果:\n条件循环语句 # for关键字 # 在Go语言中去除了while ,do - while 循环,变得更加简洁了。\n格式:\nfor 控制变量(赋值表达式); 循环条件(逻辑表达式\\关系表达式);控制变量控制(自增\\自减){ //语句 } 其中 控制变量控制 是可以选择性添加的，不添加就实现了while循环\npackage main import \u0026#34;fmt\u0026#34; func main() { var A = 0 for i := 0; i \u0026lt; 10; i++{ A = A + i } fmt.Println( \u0026#34;A = \u0026#34;,A) } 运行结果:\nfor range 语句 # for range语句有着类似迭代器操作(遍历数组，字符串等)，返回 (索引, 值) 或 (键, 值)\n特点:\n数组，切片返回格式为 \\[**索引**,**值**\\] map返回 键和值 通道(channel)只返回通道（channel）内的值 格式:\nfor (索引名\\键名),(值) := range (range的对象) { //语句 } 字符串会转为ASCII码输出，如果需要显示字符 则需要 string()转为字符串类型\npackage main import \u0026#34;fmt\u0026#34; func main() { var String = \u0026#34;ABCDEFG\u0026#34; for index,value := range String{ //index:索引 , value:for range出来的值 fmt.Println(\u0026#34;index:\u0026#34;,index,\u0026#34;,Value:\u0026#34;,string(value),\u0026#34;ASCII:\u0026#34;,value) } } 输出结果:\n流程控制语句 # Continue # 跳过当前循环，进入下一循环\npackage main import \u0026#34;fmt\u0026#34; func main() { for i := 0;i \u0026lt; 3;i++{ fmt.Println(\u0026#34;i = = = \u0026#34;,i) for n := 0;n \u0026lt; 5;n++{ if (n \u0026lt; 2){ continue } fmt.Println(\u0026#34;n =\u0026#34;,n) } } } 输出结果:\nBreak # 终止当前循环,或者跳出switch语句\n在多重循环中，可以用标号 label 标出想 break 的循环。\npackage main import \u0026#34;fmt\u0026#34; func main() { for i := 0;i \u0026lt; 3;i++{ fmt.Println(\u0026#34;i = = = \u0026#34;,i) for n := 0;n \u0026lt; 5;n++{ if (n \u0026lt; 2){ break } fmt.Println(\u0026#34;n =\u0026#34;,n) } } } 输出结果：\nbreak + label 跳出指定循环\npackage main import \u0026#34;fmt\u0026#34; func main() { label1: for i := 0; i \u0026lt; 3; i++ { for n := 0; n \u0026lt; 3; n++ { break label1 //跳出最外层循环（label1） fmt.Println(\u0026#34;循环2\u0026#34;) } fmt.Println(\u0026#34;循环1\u0026#34;) fmt.Println(\u0026#34;---------------\u0026#34;) } } 输出结果:\nGoto # 无条件地转移到过程中指定的行。\n格式 :\ngoto label; .. . label: statement; 使用Goto语句来跳出双层循环. # 未使用Goto：\npackage main import \u0026#34;fmt\u0026#34; func main() { for i := 0; i \u0026lt; 3; i++ { for n := 0; n \u0026lt; 3; n++ { fmt.Println(\u0026#34;循环2\u0026#34;) //goto label } fmt.Println(\u0026#34;循环1\u0026#34;) fmt.Println(\u0026#34;---------------\u0026#34;) } //label: //fmt.Println(\u0026#34;label\u0026#34;) } 输出结果,可见break只能跳出所在的循环，无法跳出双层循环。\n循环 2 循环1 --------------- 循环 2 循环1 --------------- 循环2 循环1 --------------- 使用Goto语句:\npackage main import \u0026#34;fmt\u0026#34; func main() { for i := 0; i \u0026lt; 3; i++ { for n := 0; n \u0026lt; 3; n++ { fmt.Println(\u0026#34;循环2\u0026#34;) goto label //跳到名为label的代码处 } fmt.Println(\u0026#34;循环1\u0026#34;) fmt.Println(\u0026#34;---------------\u0026#34;) } label: fmt.Println(\u0026#34;label\u0026#34;) } 输出结果：\n循环2 label 函数 # 何为函数 # 函数是基本的代码块，用于执行一个任务。\nGo 语言最少有个 main() 函数。\n你可以通过函数来划分不同功能，逻辑上每个函数执行的是指定的任务。\n函数声明告诉了编译器函数的名称，返回类型，和参数。\n函数的定义和调用 # 函数定义 # Go语言定义函数的语法：\nfunc function_name( [parameter list] ) [return_types] { 函数体 [return return_Numbers] } func ：定义函数的关键字\nfunction_name : 函数名\nparameter list : 参数列表，需要传入的参数和参数类型，根据需要选择可以不设置。\nreturn_types ： 返回值类型，返回一系列的值，根据需要选择可以不设置。\n函数体：函数定义的代码集合。\nfunc PrintHello() { //打印Hello的函数 var String = \u0026#34;Hello\u0026#34; fmt.Println(String) } 函数调用 # 当创建函数时，你定义了函数需要做什么，通过调用该函数来执行指定任务。\npackage main import \u0026#34;fmt\u0026#34; func main() { PrintHello() //调用PrintHello函数 fmt.Println(PrintHelloWorld()) //调用PrintHelloWorld函数 fmt.Println(Add(10, 10)) //调用求和函数 } func PrintHello() { //PrinrHello函数，无参，无返回值 var String = \u0026#34;Hello\u0026#34; fmt.Println(String) } func PrintHelloWorld() string { //PrintHelloWorld函数，无参，有返回值 var String = \u0026#34;Hello,World!\u0026#34; return String } func Add(X int, Y int) int { //求和函数，有参，有返回值 Res := X + Y return Res } 运行结果：\nHeTTo Hetto ,WorTd! 20 函数类型 # 使用Type关键字可以定义函数类型。\n在Go语言中,type可以定义任何自定义的类型。\npackage main import \u0026#34;fmt\u0026#34; func main() { type T1 func(int, int) int //使用type关键字，定义T1类型 var F T1 = Add //声明T1类型的变量F ，将Add函数赋给F f1 := F(1, 4) fmt.Println(f1) var F2 T1 = subtract f2 := F2(10, 40) fmt.Println(f2) fmt.Println(f1 + f2) } func Add(X int, Y int) int { //Add函数 Res := X + Y return Res } func subtract(X int, Y int) int { //subtract函数 Res := X - Y return Res } 输出结果：\n5 -30 -25 高阶函数 # 函数作为参数 # package main import \u0026#34;fmt\u0026#34; func main() { PrintHello(\u0026#34;Sam\u0026#34;, SayHello) } func SayHello(name string) string { var Hello string = name + \u0026#34; Hello\u0026#34; fmt.Println(\u0026#34;func SayHello: \u0026#34;, Hello) return Hello } func PrintHello(name string, f func(string2 string) string) { fmt.Println(\u0026#34;func PrintHello: \u0026#34;, name, f(name)) } 输出结果：\n函数作为返回值 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { fmt.Println(Add(10, 20)) } func Add(x int, y int) string { var Res = x + y return ToString(Res) } func ToString(z int) string { var Res = strconv.Itoa(z) //使用 strconv.Itoa()将整型数字转为字符串数字 return Res } 其他 # strconv.Itoa() 和 string()的区别\nstring()是直接整数类型的数字转为ASCII码值等于该整形数字的字符。\n而strconv.Itoa() 是转换成对应的字符串类型的数字。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { var Number = 40 fmt.Println(Number) fmt.Printf(\u0026#34;%T\\n\u0026#34;, Number) fmt.Println(\u0026#34;------------------\u0026#34;) fmt.Println(string(Number)) fmt.Printf(\u0026#34;%T\\n\u0026#34;, string(Number)) fmt.Println(\u0026#34;------------------\u0026#34;) fmt.Println(strconv.Itoa(Number)) fmt.Printf(\u0026#34;%T\\n\u0026#34;, strconv.Itoa(Number)) } 输出结果：\n匿名函数 # Go语言中的函数无法实现函数的嵌套，但是可以通过匿名函数来实现相似的效果。\n匿名函数效果类似Python中的lamda\n格式:\nfunc(参数列表) 返回值 { 函数体 } package main import \u0026#34;fmt\u0026#34; func main() { Add := func(X int, Y int) int { //匿名函数 return X + Y } A := Add(1, 2) //调用匿名函数 fmt.Println(\u0026#34;var A =\u0026#34;, A) //输出 } 输出结果：\nvar A = 3\n","date":"2022 年 05 月 28 日","externalUrl":null,"permalink":"/posts/golang%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80/","section":"Posts","summary":"","title":"Golang语言基础","type":"posts"},{"content":" 项目介绍 # 项目要求：\n使用JAVA制作爬虫，操作API爬取网页的信息，保存到本地。\n实现原理 # 使用Jsoup解析HTML，获取网站源代码，通过解析HTML获取需要的数据，并保存本地。\nMaven框架 # Maven配置 # 配置Maven环境 # 首先要去下载Maven，这里是在Maven的下载地址: Maven – Download Apache Maven,然后解压到你想要的地方，然后打开bin文件夹，复制路径\n然后配置环境变量（MAVEN_HOME 和 Path都设置成你安装目录下的bin目录 ）。\n然后Windows + R 输入 CMD,接着输入 mvn -v 来看看是否配置成功环境变量。\n如果输出版本，就说明配置成功。\n配置Maven设置 # 紧接着要开始设置Maven了\n首先打开conf文件夹，打开settings.xml\n找到 \u0026lt;localRepository\u0026gt;/path/to/local/repo\u0026lt;/localRepository\u0026gt;这句，复制出来，中间的 `/path/to/local/repo` 这句是定义自己想要的Jar包的位置，选择自己想要的位置\n然后配置Jar包下载的镜像。找到里面的 \u0026lt;mirror\u0026gt; 标签，然后改为阿里云的镜像\n\u0026lt;!--阿里云镜像--\u0026gt; \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;nexus-aliyun\u0026lt;/id\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;name\u0026gt;Nexus aliyun\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://maven.aliyun.com/nexus/content/groups/public\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; 然后保存，Maven设置完成。\nIDJA上的Maven设置 # 打开IDJA ，点击“文件”,进入\u0026quot;新项目设置\u0026quot;。（英文版为: 点击File ，进入Other Settings Of New Project \\New Project Settings）\n搜索Maven(英文版同理)。\n在”用户设置文件“导入之前在Setting.xml的设置，在“本地仓库\u0026quot;选择之前在Setting.xml中设置的Jar的路径，“Maven主路径”选择conf的上一级文件夹。（英文对照: \u0026ldquo;Maven主路径\u0026rdquo; : Maven home directopry ，\u0026ldquo;用户设置文件\u0026rdquo; : User settings file，\u0026ldquo;本地仓库\u0026rdquo; : Local repository）\n导入Setting.xml后\u0026quot;本地仓库(Local repository)\u0026ldquo;和\u0026rsquo;用户设置文件(User settings file)\u0026ldquo;后面的\u0026quot;重写\u0026rdquo;(Override)记得勾选\n然后点击\u0026quot;应用\u0026rdquo;(Apply)。\nIDJA 的 Maven 设置成功o(〃＾▽＾〃)o\nMaven目录结构 # Maven1//Maven项目名 └─src ├─main │ ├─java │ └─resources └─test ├─java └─resources Windows系统显示文件目录树 在CMD窗口下输入\nTree + [查看目录树的地址] 就可显示了\nMaven仓库依赖\n点击此链接进入Maven仓库，点击选择需要的API版本，点击在需要的API找到Maven点击即可复制。\nCMD编译和运行Maven项目 # cmd找到项目的根目录，找到maven项目的根目录，在地址框输入cmd，回车（或者win + R,输入cmd打开cmd窗口然后 cd maven项目路径 ）,在弹出的cmd窗口中输入 mvn compile 编译。输出 “BUILD SUCCESS”说明编译成功。\n我们看到了出现了这样的提示：\n是因为编码设置的不正确，这时候要打开项目的pom.xml文件，在\u0026lt;properties\u0026gt; 里加入下面这句\n\u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; 然后再运行mvn compile 就不报错啦。\n运行Maven项目\n写一个JAVA程序\npublic class Main { public static void main(String[] args){ System.out.println(\u0026#34;Hello,Maven!\u0026#34;); } } 然后在当前Cmd窗口下输入 mvn exec:java -Dexec.mainClass=\u0026quot;PC\u0026quot; 就可以运行在main问减价下的Main类中的Main方法。\nmvn exec:java -Dexec.mainClass=\u0026#34;PC\u0026#34; //注意: PC是我自己写的JAVA类名，-Dexec.mainClass会让Maven从项目目录的main文件夹开始 mvn exec:java -Dexec.mainClass=\u0026#34;\u0026#34; 编译完成输出结果：\nJAVA爬虫 # 配置到Maven框架，就可以愉快的编写JAVA爬虫了。\n爬虫 ： 按照一定的规则，自动地抓取万维网信息的程序或者脚本\n环境配置 # 前提：\nIntelliJ IDEA\nMaven框架\nJDK 1.8\n安装需要的Jar包 # 以下的所有Jar包均可在Maven仓库 https://mvnrepository.com/ 下载\n接着要安装Jsoup包，用于解析HTML并查找(均放在\u0026lt;dependencies\u0026gt;\u0026lt;/dependencies\u0026gt;标签中 )\n\u0026lt;!-- https://mvnrepository.com/artifact/org.jsoup/jsoup --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jsoup\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jsoup\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.14.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 还有SLF4J包，用来记录日志。\n\u0026lt;!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-log4j12\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.25\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Commons-io,常用的工具类\n\u0026lt;!-- https://mvnrepository.com/artifact/commons-io/commons-io --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;commons-io\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-io\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.11.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 以及处理字符串的common-lang3\n\u0026lt;!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.commons\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-lang3\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.12.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置SLF4J # 在resources下创建一个SLF4J配置文件，名为 log4j.properties\n写入配置代码：\nlog4j.rootLogger=DEBUG,A1 log4j.logger.cn.itcast = DEBUG log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss, SSS} [%t] [%c]-[%p] %m%n 至此Java的环境配置完成。\nJAVA爬虫编写 # 使用 Jsoup 实现爬取文章封面 # 分析源代码，找出封面位置,是在\u0026lt;div class=\u0026quot;cover\u0026quot;\u0026gt;中的\u0026lt;img\u0026gt;标签的data-src属性\n接下来的操作，将围绕这个\u0026lt;div\u0026gt;框和里面的\u0026lt;img\u0026gt;标签展开\nimport org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.apache.commons.io.*; import java.io.*; import java.net.URL; import java.net.URLConnection; public class Main { public static void main(String[] args) throws IOException{ /*获取HTML*/ //String Url = \u0026#34;https://xenolies.xyz/\u0026#34;; //爬取网站2 String Url = \u0026#34;https://xenolies.github.io/\u0026#34;;//需要爬取的网站URL //使用Jsoup.connect()方法链接Url,获取HTML文件 Connection connection = Jsoup.connect(Url); //设置延时，避免出现Connection reset错误 connection.timeout(3000); //使用Get(),解析HTML，使用Document来接受返回的信息 Document document = connection.get(); /*使用各种选择器来解析获取获取到的HTML*/ //使用Class选择元素，获取所有名为cover的元素 Elements cover = document.getElementsByClass(\u0026#34;cover\u0026#34;); //创建list来保存爬出的图片URL String[] UrlList = new String[cover.size()]; //for循环持续爬取图片,.size()获取循环次数 for (int Time = 0;Time \u0026lt; cover.size();Time++){ /*从获取的元素中获取数据*/ //获取属性值 //获取cover中的包含\u0026lt;img\u0026gt;标签的第几个\u0026lt;div\u0026gt;(数组排列) Element div = cover.get(Time); Elements imgUrl = div.getElementsByTag(\u0026#34;img\u0026#34;); //获取到\u0026lt;img标签\u0026gt; String ImgUrl = imgUrl.attr(\u0026#34;data-src\u0026#34;); System.out.println(\u0026#34;第\u0026#34; + (Time+1) + \u0026#34;张图片\u0026#34;); UrlList[Time] = ImgUrl; //将单个的图片地址添加进数组 } String filePath = \u0026#34;C:\\\\Users\\\\35367\\\\Desktop\\\\Webps\u0026#34; ; //图片保存的位置 for (int i = 0;i \u0026lt; cover.size();i++){ //遍历,下载 File dir = new File(filePath); //substring()方法来截取字符串，lastIndexOf()来截取最后一个 \u0026#34;/\u0026#34;出现时候的位置（要+1来跳过） String fileName = UrlList[i].substring(UrlList[i].lastIndexOf(\u0026#34;/\u0026#34;) + 1,UrlList[i].length()); //File.separator为分隔符，在什么系统使用法对应的分隔符，用于保存 File file = new File(filePath + File.separator + fileName); //输出的路径 Connection connectionIMG = Jsoup.connect(UrlList[i]); //建立新的connection URL url =new URL(UrlList[i]); //new成为Url以便后面使用 URLConnection ConnectionIMG = url.openConnection(); InputStream in = ConnectionIMG.getInputStream(); OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); byte[] buf = new byte[1024]; int len = -1; while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); } } } } 爬取结果:\n这样不是很方便整理，所以需要生成个表格。\n那就用HSSFWorkBook生成个表格。\nimport org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.*; import java.net.URL; import java.net.URLConnection; public class ImageExcel { public static void main(String[] args) throws IOException{ /*获取HTML*/ //String Url = \u0026#34;https://xenolies.xyz/\u0026#34;; //爬取网站2 String Url = \u0026#34;https://xenolies.github.io/page/2\u0026#34;;//需要爬取的网站URL Connection connection = Jsoup.connect(Url);//使用Jsoup.connect()方法链接Url,获取HTML文件 //connection.timeout(3000); //设置延时，避免出现Connection reset错误 Document document = connection.get(); //使用Get(),解析HTML，使用Document来接受返回的信息 /*使用各种选择器来解析获取获取到的HTML*/ Elements cover = document.getElementsByClass(\u0026#34;cover\u0026#34;); //使用Class选择元素，获取所有名为cover的元素 String[] UrlList = new String[cover.size()]; //创建list来保存爬出的图片URL for (int Time = 0;Time \u0026lt; cover.size();Time++){ //for循环持续爬取图片,.size()获取循环次数 /*从获取的元素中获取数据*/ //获取属性值 Element div = cover.get(Time); //获取cover中的包含\u0026lt;img\u0026gt;标签的第几个\u0026lt;div\u0026gt;(数组排列) Elements imgUrl = div.getElementsByTag(\u0026#34;img\u0026#34;); //获取到\u0026lt;img标签\u0026gt; String ImgUrl = imgUrl.attr(\u0026#34;data-src\u0026#34;); System.out.println(\u0026#34;第\u0026#34; + (Time+1) + \u0026#34;张图片\u0026#34;); System.out.println(ImgUrl);//查看输出的图片Url UrlList[Time] = ImgUrl; //将单个的图片地址添加进数组 } HSSFWorkbook hssfWorkbook = new HSSFWorkbook(); HSSFSheet hssfSheet = hssfWorkbook.createSheet(\u0026#34;Images\u0026#34;); hssfSheet.setColumnWidth(0, 10000); hssfSheet.setColumnWidth(1, 20000); String filePath = \u0026#34;C:\\\\Users\\\\35367\\\\Desktop\\\\Webps\u0026#34; ; //图片保存的位置 String ExcelPath = \u0026#34;C:\\\\Users\\\\35367\\\\Desktop\\\\ImageExcel.xls\u0026#34;; //Excal位置 for (int i = 0;i \u0026lt; cover.size();i++){ //遍历,下载 File dir = new File(filePath); //substring()方法来截取字符串，lastIndexOf()来截取最后一个 \u0026#34;/\u0026#34;出现时候的位置（要+1来跳过） String fileName = UrlList[i].substring(UrlList[i].lastIndexOf(\u0026#34;/\u0026#34;) + 1,UrlList[i].length()); if (i ==0){ HSSFRow Row = hssfSheet.createRow(i); //行 HSSFCell cell1 = Row.createCell(0); HSSFCell cell2 = Row.createCell(1); cell1.setCellValue(\u0026#34;FileName\u0026#34;); cell2.setCellValue(\u0026#34;ImageURL\u0026#34;); }else { HSSFRow Row = hssfSheet.createRow(i); //行 HSSFCell cell1 = Row.createCell(0); HSSFCell cell2 = Row.createCell(1); cell1.setCellValue(fileName); cell2.setCellValue(UrlList[i]); } //File.separator为分隔符，在什么系统使用法对应的分隔符，用于保存 File file = new File(filePath + File.separator + fileName); //输出的路径 URL url =new URL(UrlList[i]); //new成为Url以便后面使用 URLConnection ConnectionIMG = url.openConnection(); InputStream in = ConnectionIMG.getInputStream(); OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); byte[] buf = new byte[1024]; int len = -1; while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); } FileOutputStream fos = new FileOutputStream(ExcelPath); //IO流写入 hssfWorkbook.write(fos); fos.close(); System.out.println(\u0026#34;Imgage.xls创建成功\u0026#34;); System.out.println(fileName + \u0026#34; 已下载\u0026#34;); } } } 运行结果:\nJsoup + HSSFWorkbook 爬取，生成表格 # 这次爬取的网页时LR2 的段位表\nF12打开控制台，查看源代码，\n发现我们要找的东西在 \u0026lt;tr\u0026gt; 框的 \u0026lt;th\u0026gt;标签和 \u0026lt;td\u0026gt;标签里面，单纯的提取 \u0026lt;tr\u0026gt;会导致混乱，所以要加入一个判断。\n因为段位表是表格，所以我们要引入一个新的Jar包 Poi ,来制作导出表格\n\u0026lt;!-- https://mvnrepository.com/artifact/org.apache.poi/poi --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.poi\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;poi\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.1.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 使用教程可以参考这个 ：使用HSSFWorkbook导出、操作excell\n代码如下 ：\nimport org.apache.poi.hssf.usermodel.*; import org.jsoup.Connection; import org.jsoup.Jsoup ; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.FileOutputStream; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { //确认爬取的HTML网页, String url = \u0026#34;http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=gradelist\u0026#34;; //链接网页 Connection connection = Jsoup.connect(url); //获取网站源代码 Document document = connection.get(); SPRank(document); DPRank(document); } public static void SPRank(Document document) throws IOException { Elements Table = document.getElementsByTag(\u0026#34;table\u0026#34;); Element table = Table.get(0); Elements Tr = table.getElementsByTag(\u0026#34;tr\u0026#34;); //System.out.println(Tr); Elements H3 = document.getElementsByTag(\u0026#34;h3\u0026#34;); //System.out.println(H3); String h31 = H3.get(0).text(); //SP段位名 HSSFWorkbook hssfWorkbook = new HSSFWorkbook(); //创建Excal表 HSSFSheet hssfSheetSP = hssfWorkbook.createSheet(h31); //创建名为 :\u0026#34;段位認定　シングル(2018) \u0026#34; Excel //设置单元格宽度 hssfSheetSP .setColumnWidth(1, 8000); HSSFCellStyle cellStyle= hssfWorkbook.createCellStyle(); //设置水平对齐的样式为居中对齐; cellStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER); //设置字体 HSSFFont font = hssfWorkbook.createFont(); font.setFontName(\u0026#34;黑体\u0026#34;); int Th1 = 0; //列数 int Tr1 = Tr.size(); //行数 String star = \u0026#34;\u0026#34;; //小段位星级 String gradename = \u0026#34;\u0026#34;; //小段位名 String crossnum = \u0026#34;\u0026#34;; //小合格人数 String challengesnum = \u0026#34;\u0026#34;; //小挑战人数 String yield = \u0026#34;\u0026#34;; //小合格率 String Star = \u0026#34;\u0026#34;; //段位星级 String GradeName = \u0026#34;\u0026#34;; //段位名 String CrossNum = \u0026#34;\u0026#34;; //合格人数 String ChallengesNum = \u0026#34;\u0026#34;; //挑战人数 String Yield = \u0026#34;\u0026#34;; //合格率 for (int Tri = 0; Tri \u0026lt; Tr.size(); Tri++) { Element TrTitle = Tr.get(Tri); //第几个Tr框 Elements Td = TrTitle.getElementsByTag(\u0026#34;td\u0026#34;); //合格率等标题 : \u0026lt;th\u0026gt; 其他的时 : \u0026lt;td\u0026gt; Elements Th = TrTitle.getElementsByTag(\u0026#34;th\u0026#34;); HSSFRow hssfRow = hssfSheetSP.createRow(Tri); //在对应的行中填入数据，定位行 HSSFCell cell1 = hssfRow.createCell(0); //第rowi行第一个单元格 HSSFCell cell2 = hssfRow.createCell(1); HSSFCell cell3 = hssfRow.createCell(2); HSSFCell cell4 = hssfRow.createCell(3); HSSFCell cell5 = hssfRow.createCell(4); if (Td.size() == 0) { //获取小段位内容 } else { star = Td.get(0).text(); gradename = Td.get(1).text(); crossnum = Td.get(2).text(); challengesnum = Td.get(3).text(); yield = Td.get(4).text(); cell1.setCellValue(star); cell2.setCellValue(gradename); cell3.setCellValue(crossnum); cell4.setCellValue(challengesnum); cell5.setCellValue(yield); } if (Th.size() == 0) { //获取段位表内容 } else { Th1 = Th.size(); //获取列数 Star = Th.get(0).text(); GradeName = Th.get(1).text(); CrossNum = Th.get(2).text(); ChallengesNum = Th.get(3).text(); Yield = Th.get(4).text(); cell1.setCellValue(Star); cell2.setCellValue(GradeName); cell3.setCellValue(CrossNum); cell4.setCellValue(ChallengesNum); cell5.setCellValue(Yield); } //表名 : 段位認定　シングル(2018) (h31) //项目名 : 1.星级 Star 2.段位名 GradeName 3.合格人数 CrossNum 4.挑战人数 ChallengesNum 5.通过率 Yield //行数: Tr1 , 列数 : Th1 } FileOutputStream fos = new FileOutputStream(\u0026#34;C:\\\\Users\\\\35367\\\\Desktop\\\\LR2(SP).xls\u0026#34;); //IO流写入 hssfWorkbook.write(fos); fos.close(); System.out.println(\u0026#34;SPRank创建成功\u0026#34;); } public static void DPRank(Document document) throws IOException{ Elements Table = document.getElementsByTag(\u0026#34;table\u0026#34;); Element table = Table.get(1); Elements Tr = table.getElementsByTag(\u0026#34;tr\u0026#34;); //System.out.println(Tr); Elements H3 = document.getElementsByTag(\u0026#34;h3\u0026#34;); //System.out.println(H3); String h32 = H3.get(1).text(); //SP段位名 HSSFWorkbook hssfWorkbook = new HSSFWorkbook(); //创建Excal表 HSSFSheet hssfSheetDP = hssfWorkbook.createSheet(h32); //创建名为 :\u0026#34;段位認定　DP(2018) \u0026#34; Excel表 //设置单元格宽度 hssfSheetDP .setColumnWidth(1, 8400); HSSFCellStyle cellStyle= hssfWorkbook.createCellStyle(); //设置水平对齐的样式为居中对齐; cellStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER); //设置字体 HSSFFont font = hssfWorkbook.createFont(); font.setFontName(\u0026#34;黑体\u0026#34;); int Th1 = 0; //列数 int Tr1 = Tr.size(); //行数 String star = \u0026#34;\u0026#34;; //小段位星级 String gradename = \u0026#34;\u0026#34;; //小段位名 String crossnum = \u0026#34;\u0026#34;; //小合格人数 String challengesnum = \u0026#34;\u0026#34;; //小挑战人数 String yield = \u0026#34;\u0026#34;; //小合格率 String Star = \u0026#34;\u0026#34;; //段位星级 String GradeName = \u0026#34;\u0026#34;; //段位名 String CrossNum = \u0026#34;\u0026#34;; //合格人数 String ChallengesNum = \u0026#34;\u0026#34;; //挑战人数 String Yield = \u0026#34;\u0026#34;; //合格率 for (int Tri = 0; Tri \u0026lt; Tr.size(); Tri++) { Element TrTitle = Tr.get(Tri); //第几个Tr框 Elements Td = TrTitle.getElementsByTag(\u0026#34;td\u0026#34;); //合格率等标题 : \u0026lt;th\u0026gt; 其他的时 : \u0026lt;td\u0026gt; Elements Th = TrTitle.getElementsByTag(\u0026#34;th\u0026#34;); HSSFRow hssfRow = hssfSheetDP.createRow(Tri); //在对应的行中填入数据，定位行 HSSFCell cell1 = hssfRow.createCell(0); //第rowi行第一个单元格 HSSFCell cell2 = hssfRow.createCell(1); HSSFCell cell3 = hssfRow.createCell(2); HSSFCell cell4 = hssfRow.createCell(3); HSSFCell cell5 = hssfRow.createCell(4); if (Td.size() == 0) { }else { star = Td.get(0).text(); gradename = Td.get(1).text(); crossnum = Td.get(2).text(); challengesnum = Td.get(3).text(); yield = Td.get(4).text(); cell1.setCellValue(star); cell2.setCellValue(gradename); cell3.setCellValue(crossnum); cell4.setCellValue(challengesnum); cell5.setCellValue(yield); } if (Th.size() == 0) { }else { Th1 = Th.size(); Star = Th.get(0).text(); GradeName = Th.get(1).text(); CrossNum = Th.get(2).text(); ChallengesNum = Th.get(3).text(); Yield = Th.get(4).text(); cell1.setCellValue(Star); cell2.setCellValue(GradeName); cell3.setCellValue(CrossNum); cell4.setCellValue(ChallengesNum); cell5.setCellValue(Yield); } } //表名 : 段位認定　シングル(2018) (h31) //项目名 : 1.星级 Star 2.段位名 GradeName 3.合格人数 CrossNum 4.挑战人数 ChallengesNum 5.通过率 Yield //行数: Tr1 , 列数 : Th1 FileOutputStream fos = new FileOutputStream(\u0026#34;C:\\\\Users\\\\35367\\\\Desktop\\\\LR2(DP).xls\u0026#34;); //IO流写入 hssfWorkbook.write(fos); fos.close(); System.out.println(\u0026#34;DPRank创建成功\u0026#34;); } } 输出结果:\n","date":"2022 年 05 月 10 日","externalUrl":null,"permalink":"/posts/java%E7%88%AC%E8%99%AB%E9%A1%B9%E7%9B%AE%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0maven/","section":"Posts","summary":"","title":"Java爬虫项目学习笔记(maven框架)","type":"posts"},{"content":" 引子 # 马里奥的游戏总是可以给人欢乐，而这样的欢乐却和其他的游戏大为相反，比起画面，马里奥往往给人新鲜感和单纯的乐趣。因此我就以马里奥3D大陆为基础，把马里奥的设计思路总结，做个了小总结。一共总结成为了三个方面：引导(Guide)－－－\u0026gt;认知(Cognition)－－\u0026gt;突破(Breakthrough)（GCB方法），这三个一直在指导玩家，让不同层次玩家能有着不同层次的游戏方法，以及舒适的游戏体验。\n同时，也感谢Indienova中一篇文章的帮助，把我上一篇中提到的模糊概念讲的十分透彻，本文也是在这篇文章的启发下写出来的，同时也感谢译者，翻译的相当有水平，鄙人感激不尽。\n相关链接\nIndienova文章链接：使用“马里奥方法”设计游戏关卡 indienova 独立游戏\n所翻译的原文链接：How to Design Levels With the “Super Mario World Method”\n上一篇链接：我的一些关卡设计心得\n我在上文中提到了GCB方法了，以下将用马里奥3D大陆世界1的第一关来说明。\n合适的引导玩家 # 整个第一关，我将它分为三个部分。\n第一个部分是引导\n这里在马里奥第一个场景就有体现（由于实际机器无法录屏只好这样了）\n这里设计了一大块空地给玩家自己探索，可以练习跳跃等操作。在这里，玩家可以去尝试在说明书中的各种操作。\n说明书的图来自这个知乎回答 ： 《超级马里奥兄弟》为什么可以不用新手引导？ - Leon Lee的回答 - 知乎 、\n这样的引导只是玩家的基础，而我这里提到的是后面的地方\n在跳上一个坎后，碰到了一个写着问号的方块。\n这时候，很可能有玩家会跳一下，顶这个方块，这时候方块蹦出一个金币。这时候玩家会知道，带问号的黄色方块有好东西。这样一个引导完成了。\n在这里，就是我所说的引导，即为：\n一个 引导 是指让玩家了解 游戏机制 的 事件\n而下面，无不体现着任天堂的引导：\n河道上会飞的金币\n会追着玩家的板栗仔（Goomba）\n旗杆的金币\n让引导循序渐进 # 注意，这样的引导并不是一下子全部给你，有些还是要靠你发现的。\n比如下面这个场景：\n图片的左下角有个用尾巴击打会蹦出金币的砖块，这是在引导你，说如果用尾巴连续击打一些特殊砖块，会有奖励。而在接下来，就立马出现了个砖块，这时候你被引导去击打这方块。\n在这时候，出现了意想不到的事情，一个轮盘！击打它还会上升。\n而这样的情况，不属于游戏设计者主动要求你的，是你自己探索出来的，我把它称之为：认知（Cognition），在引导之上，你自己探索出来的新东西。\n一个 认知 是 玩家 在 经过引导 之后 自我探索 出来的 事件\n建立在认知之上的突破 # 这里，我们把视角转换下，从一个开发者的角度转为一个玩家的角度：我了解了一系列操作，我也知道了一些技巧。这时候，我需要有些挑战性的来试一下。所以，任天堂设计的突破，也就出现了。\n一个 突破 是 建立在玩家 认知 之上 的 有挑战性 的 事件\n而任天堂设计的，是一个环环相扣的关卡。\n这里拿第一关的过程来说明：\n玩家会在游戏过程中遇到一个结合的敌人，可以使用跳踩或者甩尾来击败敌人\n这里玩家碰到新的板栗仔，并没有什么难度\n而在之后的关卡中，玩家会遇到更大的板栗仔，而且还碰到不要掉出去和碰到敌人的尾巴这样的挑战。\n此时的玩家的心理将是个相对紧张的状态，但是由于有前面的认知，玩家会试着跳下去，踩死那只大板栗仔，这样完成一个“突破”。而在这期间玩家心理有个舒缓到紧张，再到舒缓的过程，就有了游戏的节奏。\n节奏的安排 # 游戏节奏的设计更像是写文章，所谓“文似看山不喜平”这样的道理也可以体现在游戏上。\n这里用世界1 的BOSS战来解释节奏：\n关卡前期 # 关卡前期 - 1 # 关卡前期 - 1 相对平缓\n关卡开始，主要的任务是给玩家些许紧张感，这里相对于前面关卡开场的风平浪静，加入了一个会攻击的敌人，塑造BOSS战的紧张感，同时，地图也在引导玩家前进。这样的关卡玩家在前面已经遇到，所以这里节奏是比较平缓的。\n关卡前期 - 2 # 这里出现了新的设计来激发玩家兴趣，这里由于齿轮的滚动，导致玩家会对位置判断失误，在这里开始，节奏开始加快，同时这里玩家也认识到了齿轮这一个全新的设计。\n关卡前期 - 2 节奏加快\n关卡前期 - 3 # 经历过刚才节奏的变快，任天堂并没有一直增加难度，而是设计了个有些难度的平缓段，所谓的难度就是会复活的敌人给玩家的威胁，这里的难度是在 前期 - 1 的之上的，整体节奏在向上走。\n关卡前期 - 3\n关卡中期 # 关卡中期 - 1 # 跳上一个上下移动的平台，进入到了关卡中期。这里基本是复刻前面的节奏，不过多赘述。一个略有挑战的平缓段来当个引子。\n关卡中期 - 2 # 这出现了新敌人，会向玩家扔东西，这里玩家会本能的躲避，并且利用前面关卡中学到的利用踩或者能力来打败敌人，同时也认识到了新的敌人类型，重新激发了新鲜感。\n关卡中期 - 3 # 这里进入了个平缓段，和前期节奏基本一致。\n关卡后期 # 关卡后期 - 1 # 这里任天堂设计了金币这样的奖励来鼓励玩家向上探索。，同时也利用地图来引导玩家了解新的敌人。这里因为石板怪给人强烈的压迫感，节奏加快，紧张感也上升到了新的高度。\n后面就出现了代表成就的星星徽章。\n关卡后期 - 2 # 这里任天堂在此设计了一个非常简单的过渡（只有一只不死乌龟），来用后面更大的节奏跳跃来凸显BOSS战的紧张。\n关卡后期 - 3 # 这里boss出现，节奏迅速加快。这里也分为三个阶段来提升玩家的紧张感的。\n关卡后期 - 3.1 由于BOSS是吐火球攻击的，所以玩家距离越远，玩家就有更多的反应时间，紧张感就越小。\n此时boss里玩家较远，紧张感不是很强。\n关卡后期 - 3.2 这里BOSS里玩家的距离开始缩短，玩家的紧张感上升.\n关卡后期 - 3.3 这里玩家离BOSS距离最近，紧张感最强，节奏发展称为整个关卡最快阶段，玩家的紧张感在此时也达到顶峰。\n关卡末尾 # 在此时，作为一个玩家此时是充满成就感的，为了迎合或者说配合这样的成就感，特意设计的最简单的方式：只有跳跃的关卡，同时结束的地方设计的也非常像个领奖台。\n回看 # 整个分析结束，我们来整体看一下整个游戏的节奏\n最开始的相对平缓，接着紧张，之后平缓紧张，到最后，接着突然上升到顶点之后骤然放慢。\n整个过程，开始的平缓引导，到后面需要寻找时机的“认知”关卡，再到最后充满挑战型的“突破”设计，整体如同山峦一般的引人入胜的难度曲线，真是佩服任天堂的关卡设计之出色。\n结语 # 用引导来合理的让游戏者熟悉玩法来转化玩家，利用合理设计培养玩家认知来提升综合体验，用充满挑战性的突破来满足核心玩家的需求，这样的GCB方法是我从《马里奥3D大陆》这款游戏中学到的。\n修改记录 # 2022.4.8 # 将原文中\u0026quot;引导(Guide)－－－\u0026gt;认识(Know)－－\u0026gt;突破(Breakthrough)\u0026ldquo;中的\u0026quot;认识(Know)\u0026ldquo;改为\u0026quot;认知(Cognition)\u0026rdquo; ，“认识”一词的意思为：指人的头脑对客观世界的反映 ，这个和我本来想表达的那种探索的心理活动不同，所以改为了有心理活动的认知(Cognition)一词。同时也集合了我之前了解到的“心流”(flow)的概念。游戏这样有着互动属性的一种娱乐形式，这样一种先天的具有了挑战和回馈的娱乐形式，是很容易进入到“心流”中去的。\n2022.4.20 # 更新完最后的“突破”部分，鸽了好久，咕咕咕。。。\n2022.4.21 # 重写“突破”部分\n原突破部分\n( 1 ) 将“这里，我们把视角转换下，从一个开发者的角度转为一个玩家的角度：我知道了一系列操作，我也知道了一些进阶技巧。这时候，光有这些简单的挑战是完全没意思的，我需要更有些挑战性的来试一下。所以，任天堂设计的突破，也就诞生了。”修改为 “这里，我们把视角转换下，从一个开发者的角度转为一个玩家的角度：我了解了一系列操作，我也知道了一些技巧。这时候，我需要有些挑战性的来试一下。所以，任天堂设计的突破，也就出现了。”\n( 2 ) 将“突破”部分的提示\n注意：这里的挑战不止是那种考验操作的挑战，还包括探索的挑战\n删除。\n( 3 )删除原来“突破”里“探索的挑战”和“操作的挑战”所有内容\n以下为原内容：\n探索的挑战\n这里，玩家右边的树可以爬上去，爬上去会得到一个变身狸猫状态的奖励，这里玩家建立了一个认识：树是可以爬上去的。而挑战在哪里呢，在于让玩家探索的挑战。\n这里玩家爬上树探索会发现有这样的方块\n通过方块跳上台子会发现水管\n这里的银色物品是星星徽章，就是给玩家的突破奖励了\n这时候，玩家会产生另一个成就感：就是挑战的成就感。\n操作的挑战\n这里拿世界一第二关的地牢场景来说明\n玩家需要得到星星徽章，必须注意移动的平台和吐遮挡视线的墨水。这里就构成了操作的挑战，也构成了一个突破。\n重写“节奏的安排”部分\n原内容如下：\n节奏的安排\nIndienova的这篇文章已经说过足够详尽，这里就不过多赘述了。\n文章链接： 使用“马里奥方法”设计游戏关卡 indienova 独立游戏\n2022.4.22 # 完成最后“节奏的安排”部分\n2022.5.5 # 1.修正一些错字，认知部分“ 就立马出现了给砖块，”更正为 “就立马出现了个砖块”，\n突破部分“这样完成一个突破‘’。”更正为“这样完成一个“突破”。\n2.将一些概念进行特殊化处理，这样更加适合阅读了\n","date":"2022 年 04 月 07 日","externalUrl":null,"permalink":"/posts/%E4%BA%AB%E5%8F%97%E6%8E%A2%E7%B4%A2%E7%9A%84%E4%B9%90%E8%B6%A3%E9%A9%AC%E9%87%8C%E5%A5%A53d%E5%A4%A7%E9%99%86%E5%85%B3%E5%8D%A1%E8%AE%BE%E8%AE%A1%E5%88%86%E6%9E%90/","section":"Posts","summary":"","title":"享受探索的乐趣《马里奥3d大陆》关卡设计分析","type":"posts"},{"content":"这篇文章并不是专业的，只是我自己总结体会，有错误之处，欢迎评论区指出，感激不尽。\n如何让玩家知道游戏的基本机制 # 优秀的设计往往第一时间让玩家知道游戏的玩法，如果玩家长时间不知道玩法，就会导致失去兴趣。\n拿小岛秀夫的合金装备为例，主要玩法是潜行等多样的玩法，如何体现这个玩法呢？\n其实在序章给出了答案：在序章的中间部分，会出现一个完全交给玩家的逃离任务，有两个路线可走（此时玩家手上有枪支），一为直接走下走廊，被敌人发现，然后玩家反击，跑向门口的“突突突”玩法，这里成为“突击流”;另一个是转头会找到一个被坍塌的楼梯组成的矮墙，同时末端有个洞，提示玩家趴下，然后慢慢爬到门口，这里成为“潜入流”。这两个设计直接体现了玩法，让玩家对游戏有了大概的认知。这样玩家在游玩的时候，就不会感觉难度跨越太大。\n因此，让玩家第一时间知道游戏的基本机制，才是最重要的一点。\n那么转到游戏，将要如何设计呢。\n第一关\n拿我设计的第一关为例，乍一看，感觉无非是墙，灯和地面，但是我们把它的主体提出来看看。\n第一关设计\n我设计的也是一款潜入游戏，那么如何让玩家了解到游戏的机制呢?\n玩家向左走不通，会向右走，碰到了障碍，这时候，玩家就会通过跳跃来越过障碍，这样玩家就学会了最基础的操作：移动，跳跃。\n那么如何让引入游戏机制呢？这时侯，玩家就会发现一个敌人，敌人在不断巡逻。这时候玩家在想，这个既然是敌人，我们就要杀死他。\n在这样的心理下，玩家就会思索，如何杀掉敌人，这里我引入了射击。这样玩家就会冲过去杀掉敌人。这时候，潜入的机制就开始发挥作用了。\n敌人会给玩家造成大量的伤害，这样玩家意识到，正面进攻（“突击流”）是极为困难的。这时，玩家就会思考其他的过关方法，头顶的平台就起了作用。这时候玩家就会意识到，自己要跳上平台来躲避敌人的视线，这样，玩家就了解了游戏的机制以及操作的方法。\n让玩家了解新机制 # 一个游戏如果自始至终都是单一机制会非常无聊，所以就要引入新机制来丰富游戏内容。\n这里我们以3ds上的“马里奥3d大陆”为例，如何引入新机制。\n马里奥中有个形态是“狸猫“，此时的马里奥会得到一个杀死一定范围内的敌人的效果，而在此游戏里，有一个一样的敌人，也是有一样的能力。玩家碰到这样的怪物就会死亡，这是非常有特点的从敌人上了解游戏机制的设计。\n放到游戏里面，可以这样来设计。\n第二关设计1\n这里的横杆是可以跳跃上去的，那么如何让玩家了解这个新机制呢？\n在这里，我设计了个boss，可以发射上中下三个方向子弹来攻击。\n第二关设计2\n玩家此时要躲避，就需要跳跃。这时候就会发现自己跳到了横杆上，并且借助横杆，可以跳的更高来更好的躲避boss攻击。通过这样的设计，玩家就了解到了新机制。\n让玩家运用新机制以及心里反馈 # 对于游戏来说，良好的难度曲线是并不可少的，因此难度曲线在玩家心里的反馈，就成了重中之重。\n前面两个关卡，第一关用于熟悉操作以及游戏机制，第二关用于了解新机制，第三关当然理所当然的是前面结合的关卡。\n由于当时时间，人力等等限制，就把这段设计做到了最后boss战环节，也就是第三关。同时也为 体现进入神秘组织内部的感觉，跟换了背景以及整体色调，暗调配合黑色背景给人一种神秘感。\n第三关\n为了激发玩家的兴奋点，我设计了boss发射导弹的动作，导弹对玩家是一击必杀的，但是玩家可以跳上导弹来接着攻击boss。\n第三关设计\n这个设计迫使玩家去练习，去熟悉boss的攻击节奏。同时死亡在给玩家挫败感的同时，也在激发玩家的兴奋，这样游戏的乐趣会更多，难度曲线才有了一些。\n这就是我的一些游戏设计心得，本人并非专业人士，理解和思考不是很深刻，欢迎评论区指正并相互学习。\n","date":"2022 年 03 月 15 日","externalUrl":null,"permalink":"/posts/%E6%88%91%E7%9A%84%E4%B8%80%E4%BA%9B%E5%85%B3%E5%8D%A1%E8%AE%BE%E8%AE%A1%E5%BF%83%E5%BE%97/","section":"Posts","summary":"","title":"我的一些关卡设计心得","type":"posts"},{"content":"","date":"2022 年 03 月 06 日","externalUrl":null,"permalink":"/tags/mysql/","section":"Tags","summary":"","title":"MySQL","type":"tags"},{"content":" 什么是MySQL？ # MySQL?ˋ( ° ▽、° ) 当然是没压岁钱了啊( M(没)y(压)S(岁)Q(钱)L(了) 谐音梗扣钱ψ(._. )\u0026gt;\nMySQL是关系型数据库管理系统 数据管理工具 MySQL 是如何管理数据的？ # MySQL原理图示\nMySQL不是直接管理数据的，是通过对表的管理，来管理数据的。\n(所以说MySQL是数据库的是错误的哦=￣ω￣=）\n什么是数据库? # 数据库（DataBase 简称DB ）是按照数据结构来组织，存储，管理数据的仓库。\n数据库的发展 # 手工管理 # 特点：\n数据不在计算机中长期保存 没有专门的数据管理软件，数据包需要应用程序自己掌握 不同程序之间无法共享数据 数据不具有独立性完全依赖应用程序 文件管理 # OS, Operating System\n特点：\n数据在计算机的外村设备上长期保存，可以对数据反复操作 利用文件系统，可以管理和存取 一定程度上实现了数据的独立性和共享性，但是非常薄弱 数据库管理 # DBMS, Database Management System\n数据结构化 数据共享 数据独立性高 数据统一管理与控制 关系型数据库 # Oracle , SQL Server , DB2 , MySQL 等.\n关系型数据库的二维表关系 # 二维表中每一行数据称之为一个元组，也叫记录\n属性: 二维表中的列称为属性，上图里的“编号”， “姓名” ， “年龄” 等就是属性\n域是指属性的取值范围，有些属性会存在。（像喜好这样的属性是没有域的╰(￣ω￣ｏ)）\n主键(Primary Key) ：可以理解成关键字，具有唯一性，不可以为空。\n外键(Foreign Key) : 用于关联两个表。\n一般主键存在于父表，外键存在于子表\n值 (Value) : 不唯一，可为空值。\n如何理解主键和外键?\n表一\n物品名 价格 数量 1 3 7 2 4 8 3 5 9 表二\n物品 数量 1 1 3 3 这里表一和表二相比，表一物品名的键对应的值多，并且关联两个表，表一的物品名为外键，而表二的物品名唯一，为主键。\n非关系型数据库 # Redis\nMongoDB\nSQL 语言 # 值得注意的是这些操作针对的是整个数据库，或者数据库的对象\n标准语言是SQL (英文全称：Structured Query Language)，结构化查询语言是关系数据库的标准语言。\nSQL分为四个部分:\n数据定义语言（DDL）:定义数据库，表等 CREATE语句创建数据库，删除表(对象)等\n数据操作语言(DML) ：用于对数据库进行添加，修改和删除等操作。 INSERT语句对数据库进行添加修改删除命令, UPDATE语句用于修改数据，DALETE语句用于删除数据。\n数据查询语言(DQl):用于查询数据。 SELECT语句查询数据库中一条数据或者多条数据。\n数据控制语言(DCL):用于控制用户。 GRANT 语句控制用户权限等\n值得注意的是这些操作针对的是整个数据库，或者数据库的对象。（对象=\u0026gt;表）\nMySQL的安装 # 安装教程可以根据这个帖子来安装：\nMySQL安装教程\n开启/关闭 MySQL 服务 # 使用管理员身份打开CMD，输入\nnet start mysql 可以打开MySQL服务，\n输入\nnet stop mysql 可以关闭MySQL服务\nMySQL基本操作 # 数据库基本操作 # 创建数据库 # 使用 Create database + 库名 来创建\nCreate database T1 ; #创建一个名为\u0026#34;T1\u0026#34;的数据库 创建成功会显示这个：\n使用参数 if not exists 可以在创建数据库时,出现相同数据库的时候不报错(一般情况下创建相同名称数据库会报错)。\nCreate database if not exists T1 ; #创建一个名为\u0026#34;T1\u0026#34;的数据库 当然也是可以设置数据库字符集。\n创建字符集为gb2312的数据库\nCreate database if not exists T1 default character set gb2312 ; #创建名为“T1”的数据库，字符集设定为gb2312 排序规则（校对规则）: 是指对指定字符集下不同字符的比较规则。\n查看可用的排序规则\nshow collsation ; #查看可用的排序规则 创建数据库T1，修改字符集为 utf8，更改排序规则为utf8_unicode_ci\nCreate database T1 default character set utf8 default collate utf8_unicode_ci ; #创建数据库T1，修改字符集为 utf8，更改排序规则为utf8_unicode_ci 查看数据库 # 查看数据库定义（字符集和排序规则）\nshow create database + 空格+库名\nshow create database T1 ; #查询T1数据库定义 使用数据库 # use +库名\nuse T1 ; #使用T1数据库 修改数据库 # alter database + 库名 default character set +新字符集 default collate + 新排序规则\nalter database T1 default character set gbk ;#将T1数据库的字符集改为gbk 删除数据库 # drop database +库名来删除数据库(此命令不可以用来删除数据库中的表)\ndrop database T1 ; #删除T1数据库 数据库的一些其他指令 # MySQL默认结束符为分号 (\u0026quot; ; \u0026ldquo;)\n修改结束符:\ndelimiter + 空格 + 新结束符\n修改结束符只能单次使用，重启后将转为默认\ndelimiter ！ #将结束符换为 \u0026#34;!\u0026#34; 可以用指令验证\nshow databases + 新结束符：展示数据库\nshow databases ! #展示数据库 退出MySQL：\nquit #退出数据库 exit 查看服务器信息:\nStatus + 空格 +结束符\nStatus ; #显示服务器信息 数据表基本操作 # 数据类型 # 整型 # int 占用四个字节\nbigint 占用八个字节\nTinyint 占用一个字节\nsmallint 占用两个字节\nmediumint 占用三个字节\n整型类型 占用字节 int 4 bigint 8 Tinyint 1 smallint 2 mediumint 3 适用于 ：有数值，有大小比较的。\n浮点型 # 有小数点的数值\nFloat (m,d)\ndouble(m,d)\n其中:\nm表示该数值有几位整数，\nd表示小数点后面有几位\nfloat (3,2) ; -9.99 \u0026mdash; +9.99\n字符串 # 字母，汉字，数字符号，特殊符号构成的数据对象\n适用于:身份证号，编号，电话号码。联系地址，名称等\nchar：定长字符串,长度为 0 - 255。保存char时，在右边填充空格来达到指定长度\nvarchar ：可变字符串, 长度为 0 - 65535。只保存需要的字符数，另外再加一个字节来记录\n如果超出规定长度，char 和 varchar 会对值裁剪以适应，如果裁剪的字符不是空格，会发出警告。\n如果超出规定长度，char 和 varchar 会对值裁剪以适应，如果裁剪的字符不是空格，会发出警告。\nblob 和text类型 # (非二进制字符串）\n存储声音，视频，头像等设置\n时间和日期类型 # date , time 和 datatime\n固定的格式数据，存储的是日期和时间\ndate类型\n存储格式 ： yyyy - mm - dd\n支持范围 ： 1000 - 01 - 01 ~~9999 - 12 - 31\ntime类型\n存储格式 : hh : mm : ss\n支持范围 ; -838 : 59 : 59 ~~ 838 : 59 : 59\ndatetime类型\n表示日期时间\n存储格式 : yyyy - mm - dd hh : mm : ss\n支持范围 ：1000 - 01 - 01 00 : 00 : 00 ~~ 9999 - 12 -31 23 : 59 :59\ntimestamp类型\n客户端插入的时间从当前时区转化为UTC（世界标准时间）进行存储。查询时，将其又转化为客户端当前时区进行返回\n创建数据表 # 在数据库中新建数据表\ncreate table t1 ; #创建名为 t1 数据表 创建数据表,规定长度\n创建前记得使用数据库\nuse t1 ; //使用t1数据库 create table t0 (name char(10), logintime date, mail varchar(20), score float(5,2), list int(5), photo text comment\u0026#34;头像\u0026#34;); #创建名为 t0 数据表 comment ： 解释。一般放在数据类型后面，表示对于字段名的解释,可以理解成为注释。\ncomment \u0026#34;解释内容\u0026#34; #comment使用方法 创建临时表\ncreate temporary table (N1) ; create temporary table () ; //创建临时数据表 N1包含两个字段：id 和 name\n查看数据表 # 查找数据表 # XX为需要查找名\nshow table like \u0026#34;XX%\u0026#34; ; //可以查找多个字符,只要指定包含查找字段就会被查找到 //如果查找 \u0026#34;X%\u0026#34;,则会出现 XXX,XYX,XXYXY,XXYXYX show table like\u0026#34;XX_\u0026#34; ; //只能查找单个字符 //如果查找\u0026#34;X_\u0026#34;,则会出现XX, 查看数据表的列 # //desc + 表名 +列名 ; //查看teacher表中的teachearID列 desc teacher teacherID ; 查看数据表状况 # show table status ; //查看数据表 但是如果数据表太多会难以查找，需要在末尾加个\\G，变为横向排列\nshow table status \\G 如果加了 \\G ，就不需要结束符了，否则会报错\n查看存在数据表的名称\nshow table + 表名 ; #查看存在数据表的名称 查看数据表结构\ndesc/describe + 表名 ; #查看数据表结构 查看表的定义语句 # 查看表里有什么\n//show create table + 表名 ; show create table score ; 查看表的结构 # 使用describe可查看字段定义，键的信息，默认值等\n//desc / describe + 表名 ; describe score ; 根据指定条配件来查询 # 消除结果集中的重复行\n使用 Distinct 关键字\nSELECT DISTINCT [字段名1，字段名2] FROM 表名 ; 多个字段名的话，只有两个字段完全相同。DISTINCT关键字才会同时消除\n实现有条件筛选\n运算符 名称 说明 = 等于 \u0026lt;\u0026gt; != 不等于 \u0026lt; \u0026lt;= \u0026gt; \u0026gt;= 小于，小于等于，大于，大于等于 Between 在\u0026hellip;..之间 is nul 空值 SELECT column_name,column_name FROM table_name [WHERE Clause] [LIMIT N][ OFFSET M 多条件查询\nWhere条件 说明 And 两个或者多个条件同时满足 Or 两个或者多个条件满足一个 In 多个条件中的一个 Like 搭配%通配符或者_通配符来模糊搜索 修改数据表 # alter table + 表名\nalter table + 表名 ； \\[warning\\]注意，以下修改数据表所有操作都要加alter table + 表名。\n\\[/warning\\] 添加新字段 # alter table 表名 add 字段名 + 字段名定义 ； 可以在末尾加first 或者 aftear 来在指定字段前新增字段\n添加默认值和删除默认值 # alter table 表名 alter 字段名 set default + 默认值 ; //修改默认值 alter table 表名 alter 字段名 drop default ; // 删除默认值 修改字段名 # alter table 表名 change 旧字段名 + 新字段名 + 字段 ; 修改字段定义 # alter table 表名 modify + 字段名 + 字段的新定义 ; alter table student modify studentmark text ; 删除字段 # alter table 表名 drop + 字段名 ; 删除多个字段\nalter table 表名 drop 字段1 drop 字段2 ; 为表重命名 # alter table 表名 rename to + 新表名 ; 实例\n为表添加备注字段，字段名为studentremark,数据类型为varcgar(100)，放置在stuname字段之后\nalter table score add studentremark varchar(100) after stuname ; 为score添加一个字段，字段名为classid 数据类型为varchar(10)，默认值为10\nalter table score add classid varchar(10) set default 10 after studentremark ; 验证\n#desc + 表名 ; desc score ; 为字段增加内容 # insert into 表名 values(内容1，内容2); 分字段批量增加内容\ninsert into 表名(字段名1，字段名2)values(内容1,内容2) ; 添加多组内容\ninsert into 表名 values(内容1,内容2)(内容3,内容4) ; 将指定字段导入到新表中\n//insert into 目标表(字段1，字段2) select (字段1，字段2) from 来源表 insert into student(Name，StudentID) select (Name，StudentID) from StuentClass update修改表的数据 # //update 表名 set 字段名1 = 值1，字段名2 = 值2 where 条件 \\[warning\\]update修改的是表中数据，不是字段\n\\[/warning\\]多表数据修改\nupdate 表名列表 set 表名.字段名1 = 值1, 表名.字段名2 = 值2 where 表名.条件 ； 表名.字段名 表示表中的某个字段\n按条件删除数据 # delete , truncate来删除\n使用delete 删除\n删除表中++所有数据：\n//delete from 表名 ; 删除表中的部分数据\ndelete from 表名 where 条件 ； 区别\nDelete: 删除部分数据或者所有数据，执行速度较慢，删除后日志中可找回\ntruncate：删除表中所有数据，删除速度较快，不能使用where子句，无法找回\n复制数据表 # create \\[if not exists\\]新表名\n\\[like 参照表名\\]\\[al {selsct 语句}\\]like 像：创建一个和草诏表名结果相同的新表，不复制内容，其他的都复制\nas 完全复制表的内容，单索引和完整性拘束不会复制，selsct语句是一个表达式，可以是一个selsc语句\nuse bookstore; create table book like book_copy1; create table book_copy2 as(select * from book); 删除数据表 # dorp table \\[if not exists\\] + 表名\ndorp table if not exists student ; 其他 # 存储引擎：对不同表类型的处理器\n默认:innoDB\nEngine = innoDB\ncreate table (字段名 数据类型)engine = innoDB ; 让其识别中文字符\nset names gbk ; 约束 # 何为约束? # 对字段添加限制，称为约束。\n约束的增加 # \\[collapse title=\"约束关键字以及定义语句\"\\]主体部分，由于对于字段取值的各种不同的限定，因此，表中的约束也分为若干种，每种约束的关键字不同。约束的定义语句也不同。\n\\[/collapse\\]constraint 约束名 创建表的时候，直接为字段定义约束\ncreate table 表名(字段 数据类型 [约束定义语句])； 创建表后，修改字段定义增加约束\nalter table 表名 add[constraint + 约束名]+约束的定义语句 主键约束(PRIMARY KEY) # 确定表中的标识列（主键字段不能为空，必须唯一）\n定义主键约束\ncreate table DDD( DDDid char(10) primary key, DDDname varchar(10));//创建名为DDD的表。创建时，将DDDid约束为主键 create table DDD( DDDid char(10), DDDname varchar(10), primary key(DDDid));//创建表后，约束主键 为约束命名\ncreate table DDD( DDDid char(10), DDDname varchar(10), primary key(DDDid), constraint pk_id primary key(DDDid))； 组合主键\n表中主键字段需要多个字段组合起来充当，来满足主键约束的要求\n特征 ： 每个字段允许有重复值，但是组合在一起不许重复，而组合中每个字段都不可以取空值，\n格式 :\n[constraint 约束名]primary key[(字段名1，字段名2)]; 注意：由于组合主键萨河及多个字段因此，组合主键只能定义表级约束，定义在表的最后或者被约束字段完成之后。\n唯一约束(UNIQUE) # 不可重复，可以有空值。\n格式:\n--建表时添加约束 CREATE TABLE 表名( 字段名 int(10) UNIQUE); --定义唯一约束 --建表后添加约束 ALTER TABLE 表名 ADD CONSTRAINT 唯一约束名 UNIQUE 字段名 ; unique index_name [字段名]; --索引命名 非空约束（NOT NULL） # 不允许空值\n如果省略关键字，默认值为null\nnot null 只能放在字符后面\n--建表时添加 Create table Student( Name varchar(100) not null )； --定义非空约束 --建表后添加 ALTER TABLE student MODIFY Name varchar(100) NOT NULL ; --MODIFY 添加非空约束 检查约束（CHECK） # 控制特定列中的值的完整性约束\n使用场景：\n订购册数 ：（1-100），出版时间等。\n--建表时添加约束 create table s2( 学号 char(10) not null, 性别 char(10) not null, check ( 学号 in ( select 学号 from student))); --建表后添加约束 ALTER TABLE s2 ADD CONSRAINT 检查约束名 CHECK ( 学号 in ( select 学号 from student)); 外键约束(FOREIGN KEY) # foreign key 定义在子表中。\n一个表中的某一个字段是参考另一个表中某个字段的取值，被称为外键约束。\n受约束的称为子表，约束子表的表称为父表。\n四个限制\n子表中的外键字段必须和父表中的参考字段的数据类型一致。 父表中的参考字段必须被主键约束或者唯一键约束，才能约束子表中的外键约束 父表中的主键值一旦被子表参照，那么这些值就不能随意修改和删除。 子表的外键字段的值可以为空值，但是如果有值必须是在父表的参照列的取值范围内 --建表时定义 CREATE TABLE 表名( 字段名 int(10), CONSTRAINT 外键约束名 FOREIGN KEY 字段名 REFERENCES 主表名[主表字段1,主表字段2] ; --建表后定义 ALTER TABLE 表名 ADD CONSTRAINT 外键约束名 FOREIGN KEY 字段名 REFERENCES 主表名[主表字段1,主表字段2] ; 默认约束(DEFAULT) # 如果该行没有指定数据，那么系统将默认值赋给该列，如果我们不设置默认值，系统默认为NULL。\n定义默认约束\n--建表时定义 CREATE TABLE 表名( 字段名 int(10) default 10); --10为默认值 --建表后定义 ALTER TABLE 表名 ADD DEFAULT 10 FOR 字段名 ; 为表添加数据 # // insert into s1(Name,ID) selest(Name,ID)from student； ","date":"2022 年 03 月 06 日","externalUrl":null,"permalink":"/posts/mysql%E5%9F%BA%E7%A1%80/","section":"Posts","summary":"","title":"MySQL基础","type":"posts"},{"content":" 什么是JAVA # 什么是JAVA？不就是杯咖啡吗(○｀ 3′○)( 谁说的，有可能还是个妹子╰(￣ω￣ｏ)（（（\njava是纯面向对象编程的语言。亲身经历：学JAVA无法解决单身问题\n如何理解面向对象编程？ # 以下转自博客园的这个帖子。\n例如我们设计一个桌球游戏（略过开球，只考虑中间过程）\nA：面向过程方式思考：\n把下述的步骤通过函数一步一步实现，这个需求就完成了。（只为演示概念，不细究逻辑问题）。\n① palyer1 击球 —— ② 实现画面击球效果 —— ③ 判断是否进球及有效 —— ④ palyer2击球\n⑤ 实现画面击球效果 —— ⑥ 判断是否进球及有效 —— ⑦ 返回步骤 1—— ⑧ 输出游戏结果\nB：面向对象方式思考：\n经过观察我们可以看到，其实在上面的流程中存在很多共性的地方，所以我们将这些共性部分全集中起来，做成一个通用的结构\n玩家系统：包括 palyer1 和 palyer2 击球效果系统：负责展示给用户游戏时的画面 规则系统：判断是否犯规，输赢等 我个人结合博客园帖子的理解:\n面向对象更像是做一个锤子，一个普遍性工具，一个实现循环显示数字的代码可以到多个地方应用，比如显示数字，让小灯循环亮起。需要这个功能调用这个类就可以了，方便快捷。但是缺点显而易见的，锤子不可能适合砸所有东西，这样面向对象也是，需要对应的库来实现。\n面向过程更像是定制化，效率高，但是添加新东西，需要写代码，相当于造一个专用的扳手。\n向世界问好 # 代码 # public class HelloWorld{ //JAVA系统的类，文件名必须和类名相同 public static void main(String[]args){ //定义main方法(函数)， //一个 java 程序运行必须而且有且仅有一个 main 方法。 //JAVA程序开始,String(str)表明数据类型,args表示数组 System.out.println(\u0026#34;hello,world\u0026#34;); //显示HelloWorld } //注意：方法必须用大括号括起来 } 注意: Java 是大小写敏感的，这就意味着标识符 Hello 与 hello 是不同的\n编译JAVA # win+R进入命令行，cd到Java文件所在目录，输入 javac + 文件名(带扩展名)编译。\nC: Users 35367 Desktop\u0026#39;TT\u0026gt;javac Hello.java 运行 # 编译完成后，输入java + 文件名（不带后缀），运行\n可能出现的问题 # 有的时候编译完成，运行，会出现这样的问题，那是编码方式不对。\n就需要修改编码格式\n另存为ANSI格式即可正常运行\n安装Eclipse # 任何东西只要娘化就会非常有趣，比如Eclipse娘 所以这才是程序员真正的快乐吗？\n具体的安装可以参考这个教程：\nEclipse安装使用教程!\n当然 IDEA也不错,此教程是使用IDEA完成的\nJAVA基本语法 # 分隔符 # 分号(\u0026quot;；\u0026quot;)（英文）：语句结束标记，for循环看分割不同成分 。\n圆点（“.”）：用于分割“类”中的函数，可以理解为Python里的 os.listdir 这段里圆点的作用(调用OS模块里面的listdir函数)\n空格（\u0026quot; \u0026ldquo;）：分割源代码里的不同部分,例如 int a; int b;\n花括号（“}”）：用于限定某一部分的范围，成对使用\n例如：\npublic class Text { piblic static void main (String[]args){ System.out.println(\u0026#34;Hello\u0026#34;); } } 标识符 # 起到表示作用的符号。(用来给类、对象、方法、变量、接口和自定义数据类型命名的。)\n命名规则：由数字，字母和下划线（_），美元符号（$）或人民币符号（￥）组成。\n例如:\nmyName，My_name，Points，$points,_sys_ta，OK，_23b，_3_\n非法标识符:\n25name，class\u0026amp;time，if\nJAVA关键字 # Java 关键字类别 Java 关键字 关键字含义 访问控制 private 一种访问控制方式：私用模式，访问控制修饰符，可以应用于类、方法或字段（在类中声明的变量） 访问控制 protected 一种访问控制方式：保护模式，可以应用于类、方法或字段（在类中声明的变量）的访问控制修饰符 访问控制 public 一种访问控制方式：共用模式，可以应用于类、方法或字段（在类中声明的变量）的访问控制修饰符。 类、方法和变量修饰符 abstract 表明类或者成员方法具有抽象属性，用于修改类或方法 类、方法和变量修饰符 class 声明一个类，用来声明新的 Java 类 类、方法和变量修饰符 extends 表明一个类型是另一个类型的子类型。对于类，可以是另一个类或者抽象类；对于接口，可以是另一个接口 类、方法和变量修饰符 final 用来说明最终属性，表明一个类不能派生出子类，或者成员方法不能被覆盖，或者成员域的值不能被改变，用来定义常量 类、方法和变量修饰符 implements 表明一个类实现了给定的接口 类、方法和变量修饰符 interface 接口 类、方法和变量修饰符 native 用来声明一个方法是由与计算机相关的语言（如 C/C++/FORTRAN 语言）实现的 类、方法和变量修饰符 new 用来创建新实例对象 类、方法和变量修饰符 static 表明具有静态属性 类、方法和变量修饰符 strictfp 用来声明 FP_strict（单精度或双精度浮点数）表达式遵循 IEEE 754 算术规范 类、方法和变量修饰符 synchronized 表明一段代码需要同步执行 类、方法和变量修饰符 transient 声明不用序列化的成员域 类、方法和变量修饰符 volatile 表明两个或者多个变量必须同步地发生变化 程序控制 break 提前跳出一个块 程序控制 continue 回到一个块的开始处 程序控制 return 从成员方法中返回数据 程序控制 do 用在 do-while 循环结构中 程序控制 while 用在循环结构中 程序控制 if 条件语句的引导词 程序控制 else 用在条件语句中，表明当条件不成立时的分支 程序控制 for 一种循环结构的引导词 程序控制 instanceof 用来测试一个对象是否是指定类型的实例对象 程序控制 switch 分支语句结构的引导词 程序控制 case 用在 switch 语句之中，表示其中的一个分支 程序控制 default 默认，例如：用在 switch 语句中，表明一个默认的分支。Java8 中也作用于声明接口函数的默认实现 错误处理 try 尝试一个可能抛出异常的程序块 错误处理 catch 用在异常处理中，用来捕捉异常 错误处理 throw 抛出一个异常 错误处理 throws 声明在当前定义的成员方法中所有需要抛出的异常 包相关 import 表明要访问指定的类或包 包相关 package 包 基本类型 boolean 基本数据类型之一，声明布尔类型的关键字 基本类型 byte 基本数据类型之一，字节类型 基本类型 char 基本数据类型之一，字符类型 基本类型 double 基本数据类型之一，双精度浮点数类型 基本类型 float 基本数据类型之一，单精度浮点数类型 基本类型 int 基本数据类型之一，整数类型 基本类型 long 基本数据类型之一，长整数类型 基本类型 short 基本数据类型之一，短整数类型 基本类型 null 空，表示无值，不能将 null 赋给原始类型（byte、short、int、long、char、float、double、boolean）变量 基本类型 true 真，boolean 变量的两个合法值中的一个 基本类型 false 假，boolean 变量的两个合法值之一 变量引用 super 表明当前对象的父类型的引用或者父类型的构造方法 变量引用 this 指向当前实例对象的引用，用于引用当前实例 变量引用 void 声明当前成员方法没有返回值，void 可以用作方法的返回类型，以指示该方法不返回值 保留字 goto 保留关键字，没有具体含义 保留字 const 保留关键字，没有具体含义，是一个类型修饰符，使用 const 声明的对象不能更新 上表转自此帖子\n注释 # 单行注释\npublic class Text{ //这是单行注释 多行注释\npublic class Text{ public static viod main (String[]args){/*这是多行注释 这是多行注释*/ System.out.println(\u0026#34;hello,world\u0026#34;); } } 文档注释\n/** 这是文档注释 这是文档注释 */ public class HelloWorld{ public static void main(String[]args){ System.out.println(\u0026#34;hello,world\u0026#34;); } } JAVA数据类型 # 基本数据类型 # 整型 # byte , short , int , log\n//整型 int a = 1 ; byte b = 2 ; // 最大表示到127 short c = 3 ; long d = 4 ; 浮点型 # float , double\n//浮点型 double e = 0.1; //双精度浮点型 float f = 0.2f ; //单精度浮点型 double e1 = 0.1d; \\[collapse title=\"在JAVA中，如果定义float类型，需要后面加个f，否则会默认转化为double类型\"\\]或者直接定义为double也可以\n\\[/collapse\\]定义double可以加d, 例如 double a = 0.455d\n字符型 # char型，单个符号，需要用单引号引起来\n//字符型 char c1 = \u0026#39;R\u0026#39;; char c2 = \u0026#39;T\u0026#39;; 如果使用字符串 String，不会报错，但是不会输出(String数据类型是引用数据类型)。\nString A = \u0026#34;String\u0026#34; //字符串,记得用双引号引起来 在JAVA中，字符可以定义成为字符串，整体的字符有些可以定义成字符，例如换行符 \u0026ldquo;\\n\u0026rdquo;\n布尔类型 # true 和 false\nboolean b1 = true ; boolean b2 = false ; 代码表示 # //整型 int a = 1 ; byte b = 2 ; short c = 3 ; long d = 4 ; //浮点型 double e = 0.1; float f = 0.2f ; //在JAVA中，如果定义float类型，需要后面加个f，否则会默认转化为double类型 //字符型 char g = \u0026#39;G\u0026#39;; //布尔型 boolean h = true; //字符串 String i = \u0026#34;Hedio_Kojima\u0026#34; 引用数据类型 # 类（对象）\n接口\n数组\n常量和变量 # 常量 # 常量值又称为字面常量，它是通过数据直接表示的，因此有很多种数据类型，像整型和字符串型等。\n整型常量 # 整型常量是整数类型的数据。它的表现有四种：\n二进制，八进制，十进制，十六进制\n例如:\n二进制 1111 八进制 17 十进制 15 十六进制 f 整型常量\nint i1 = 012 ; //八进制常量，以0开始 int i2 = 0x12 ; //十六进制常量，以0x开始 int i3 = 10 ; // 十进制常量，以0开始 浮点数常量 # 浮点数常量就是在数学中用到的小数。分为单浮点(float)和双浮点(double)。\n例如\n0.2233f\n0.2333d\n布尔常量 # 只有两个值， ture (真) 和 false (假)\n字符常量 # 字符型常量值是用单引号引起来的一个字符,例如 \u0026rsquo;e\u0026rsquo; , \u0026lsquo;a\u0026rsquo; , \u0026lsquo;E\u0026rsquo;。\n与Python不同的是，JAVA里的单引号和双引号不能混用，双引号是用来表示字符串的\n变量 # 变量的声明 # 格式 # //数据类型 变量名 = 初值 int a = 234 其中：\nint 是数据类型，\na 是变量名，\n234　为　初值\n作用域 # 也称变量的作用范围，即一个变量在多大的范围内可以使用。\n类变量 # 独立于方法之外的变量，用 static 修饰。\npublic class LBL { static int p=100; //类变量 public static void main(String[] args) { System.out.println(p); } } 实例变量 # 独立于方法之外的变量，不过没有 static 修饰。\npublic class LBL { String p=\u0026#34;hello world\u0026#34;; // 实例变量 public static void main(String[] args) { System.out.println(p); } } 局部变量 # 类的方法中的变量.\npublic class LBL { public static void main(String[] args) { int p = 123 ; System.out.println(p); } } 相对关系 # public class Text{ static int p1=0; // 类变量 String p2=\u0026#34;hello world\u0026#34;; // 实例变量 public void method(){ int p3 =0; // 局部变量 } } 运算符和表达式 # 运算符 # 运算符分类 # 算数运算符 # + （加）, - （减） , * （乘） , / （除） , %（取模） , ++ (自增,加一) , \u0026ndash; （自减，减一）,\n关系运算符 # \u0026gt; （大于）, \u0026lt; （小于） , \u0026gt;=（大于等于） , ==（等于） , != （不等于）\n逻辑运算符 # \u0026amp;\u0026amp;(与) ， （或） ，!(非) ,^（抑或）\n条件运算符 # 表示为 “ ? : ” ，是JAVA中唯一一个三目运算符，\n\u0026lt;1\u0026gt; ? \u0026lt;2\u0026gt; : \u0026lt;3\u0026gt;\n1为真，1赋为2的值，1为假，1赋为3的值。\npublic static void main(String[] args) { int x = 5 ; int y = 10 ; int z ; z = y \u0026gt; x ? y : x ; // y \u0026gt; x 为true, 返回10 System.out.println(z); } 输出结果:\n10 运算符优先级 # 优先级 运算符 结合性 1 ()、[]、{} 从左向右 2 !、+、-、~、++、-- 从右向左 3 *、/、% 从左向右 4 +、- 从左向右 5 «、»、\u0026raquo;\u0026gt; 从左向右 6 \u0026lt;、\u0026lt;=、\u0026gt;、\u0026gt;=、instanceof 从左向右 7 ==、!= 从左向右 8 \u0026amp; 从左向右 9 ^ 从左向右 10 | 从左向右 11 \u0026amp;\u0026amp; 从左向右 12 || 从左向右 13 ?: 从右向左 14 =、+=、-=、*=、/=、\u0026amp;=、|=、^=、~=、«=、»=、\u0026raquo;\u0026gt;= 从右向左 此表来自这篇文章\n其他 # 在运算中 a++ 和 ++a 的区别 # 如果是a++，a先参加运算然后在加一，和优先级无关，如果是++a，则反，\n前后自增自减运算规则 # 前自增 / 自减（++a , \u0026ndash;a）的运算规则是：先进行自增或者自减运算，再进行表达式运算；\n后自增 / 自减(a++ , a\u0026ndash;)的运算规则是：先进行表达式运算，再进行自增或者自减运算。\n上面的运算规则来自 这篇文章\npublic class Demo { public static void main(String[] args) { int x = 5; int y = 10; int z; z = ++x + y++; System.out.println(z); } } // 1+5+10,运算完成后，y+1 输出结果:\n16 表达式 # 表达式是根据 Java 语法由变量、运算符和方法调用组成的结构，表达式的结算结果为单个值。\n表达式实例 # public class Expression { public static void main(String[] args) { int a = 10, b = 20; // (a + b) * 2 就是一个算数表达式 int c = (a + b) * 2; // c \u0026gt; b 就是一个布尔表达式 if (c \u0026gt; b) { System.out.println(\u0026#34;c大于b\u0026#34;); } } } 表达式中的数据类型转换 # 转换规律：\n高精度（float,double等）不可以向低精度(int,long,byte等)转换\n可以从低精度向着高精度转换\n表达式中的数据类型转换 自动类型转换:当不同类型的常量和变量在表达式中混合使用时，它们最终将被转换为同一类型，然后进行运算。为了保证精度，转换从表示数的范围较小的数据类型到表示数的范围较大的数据类型 自动类型转换规则如下\n(byte或short)和int\u0026gt;int (byte或short或int)和long\u0026gt;long (byte或short或int或long)和float\u0026gt;float (byte或short或int或long或float)和double\u0026gt;double char和int-\u0026gt;int\n流程控制语句 # 顺序结构 # java的基本结构就是顺序结构，除非特别指明，否则就按照顺序一句一句执行\n顺序结构是最简单的算法结构\n语句与语句之间，框与框之间是按照从上到下的顺序进行的，它是由若干个依次执行的处理步骤组成的，它是任何一个算法都离不开的一种基本算法结构。\npublic class Demo { //创建demo类 public static void main(String[] args){ //main方法 System.out.println(\u0026#34;1\u0026#34;); //顺序结构将从上往下依次进行 System.out.println(\u0026#34;2\u0026#34;); System.out.println(\u0026#34;3\u0026#34;); System.out.println(\u0026#34;4\u0026#34;); System.out.println(\u0026#34;5\u0026#34;); } } 分支语句 # if语句 # 一个 if 语句包含一个布尔表达式和一条或多条语句。\n单if 语句 # /** if (logic expression){ statments } */ public class Demo { public static void main(String[] args){ int a = 10 ; if (a \u0026gt; 1) { //单选择if System.out.println(\u0026#34;a \u0026gt; 1 \u0026#34;) ; } } } if - else语句 # if 和else之间不能有任何其他语句(else if 这样的选择语句除外)\n/** if (logic expression){ // logic expression为布尔类型 statments } else{ statments } */ public static void main(String[] args) { int a = 10 ; int b = 5 ; boolean c = true ; boolean d = false ; if (a \u0026lt; b) { //if 语句分支 System.out.print(c); } else { // else 语句分支 System.out.println(d); } } if - else if - else 语句 # /** if (logic expression){ // logic expression为布尔类型 statments } else if (logic expression){ statments } else{ statments } */ public static void main(String[] args) { int a = 10; int b = 5; int c = 10; boolean d = true; boolean e = false; if (a == b) { // if分支 System.out.print(d); } else if (a == c) { // else if 分支 System.out.println(\u0026#34;a == 10\u0026#34;); } else { // else 分支 System.out.println(e); } } 嵌套if语句 # /** if (logic expression){ if (logic expression){ statments } } */ public class Demo { public static void main(String[] args){ int a = 9 ; if (a == 9){ // if语句A if (a != 10){ // if语句B` System.out.println(\u0026#34;Ture\u0026#34;); } } } } switch语句 # switch case 语句判断一个变量与一系列值中某个值是否相等，每个值称为一个分支。\nswitch 语句中的变量类型可以是： byte、short、int 或者 char。 switch 支持字符串 String 类型了，同时 case 标签必须为字符串常量或字面量。\n/** switch (expression){ case value1 : statments break;//用于控制单个选项输出,否则就会输出剩下的语句 case value2 : statments break; default:{ //前面case都没有执行的话运行 statments } */ public class Demo { public static void main(String[] args) { int a = 9 ; switch (a){ //switch语句 case 8 : //case a System.out.println(\u0026#34;a = 8\u0026#34;); break;//用于控制单个选项输出 case 9 : //case b System.out.println(\u0026#34;a = 9\u0026#34;); break; default :{ System.out.println(\u0026#34;a = default\u0026#34;); } } } } 循环语句 # while循环语句 # while循环特点：先判断是否满足循环条件，然后进入循环\n/** while (条件) { //语句 } */ public class Demo { public static void main(String[] args) { int a = 0 ; while (a \u0026lt; 10) { //while循环，判断变量a 小于零，执行循环 a++; System.out.println(a); } } } while循环练习 # 100以内的奇数和以及偶数和\npublic class Demo1 { public static void main(String[] args) { int x = 0 ; int x1 = 0 ; int y1 = 0 ; while (x\u0026lt;100){ if (x % 2 == 0) { //偶数 x1 = x1 + x ; } else { y1 = y1 + x ; //奇数 } x++ ; } System.out.println(\u0026#34;偶数和\u0026#34; + x1); System.out.println(\u0026#34;奇数和\u0026#34; + y1); } } do - while循环语句 # do - while 循环特点 ：至少循环一次，先执行后判断。先do再while\n至少循环一次是do - while语句和while语句主要的差别\n/** do{ //代码 }while(不布尔表达式); */ public class Demo { public static void main(String[] args) { int a = 0 ; int b = 0 ; do{ ++a ; //是do - while语句，a将输出10 }while (a\u0026lt;10); System.out.println(a); while(a\u0026lt;0); //while语句，直接跳出循环，b为0 } System.out.println(b); } } do-while 循环练习 # 100以内的奇数和以及偶数和\npublic class Demo1 { public static void main(String[] args) { int x = 0; int x1 = 0; int y1 = 0; do { if (x % 2 == 0) { x1 = x1 + x; } else { y1 = y1 + x; } x++; }while (x \u0026lt; 100) ; //)~100 System.out.println(\u0026#34;偶数和\u0026#34; + x1); System.out.println(\u0026#34;奇数和\u0026#34; + y1); } } for循环语句 # 格式\nfor(初始化，布尔表达式，迭代){\n代码语句\n}\n/** for (初始化，布尔表达式,迭代){ //代码语句 } */ public class Demo { public static void main(String[] args) { for ( int a = 0 ; a\u0026lt;10 ; a++){ //for (初始化,布尔表达式,迭代) System.out.println(a); //输出a } } } 多变量\npublic class Demo1 { public static void main(String[] args) { for(int a=0,b=0,c=0;a\u0026lt;10 \u0026amp;\u0026amp; b\u0026lt;10 \u0026amp;\u0026amp; c\u0026lt;10;a++,b++,c++) { System.out.println(a) ; System.out.println(b) ; System.out.println(c) ; } } } for循环练习 # 100以内的奇数和以及偶数和\npublic class Demo1 { public static void main(String[] args) { int x1 = 0 ; int y1 = 0 ; for (int x = 0;x \u0026lt; 100; x++){ if (x % 2 == 0){ x1 = x1 + x ; } } for (int y = 0 ;y \u0026lt; 100 ;y++){ if (y % 2 == 1){ y1 = y1 + y ; } } System.out.println(\u0026#34;偶数和\u0026#34; + x1); System.out.println(\u0026#34;奇数和\u0026#34; + y1); } } 嵌套for循环打印九九乘法表\npublic class TTT { public static void main(String[] args) { for (int a = 1;a \u0026lt; 10;a++){ for (int b = 1; b \u0026lt;= a;b++){ System.out.print(b + \u0026#34;X\u0026#34; + a + \u0026#34;=\u0026#34; + a*b + \u0026#34;\\t\u0026#34;); } System.out.println(); } } } 增强for循环 # 格式\nfor (声明语句 : 表达式){\n//语句\n}\npublic class Demo1 { public static void main(String[] args) { int [] num = {1,2,3,4,5,6,7,8,9,0} ; //定义数组 for ( int x : num ){ //for (声明语句:表达式) System.out.println(x); } } } break 语句 # 用来终止循环\npublic static void main(String[] args) { for (int x = 0; x \u0026lt;= 100; x++) { System.out.println(x); if (x == 10) { break; } } } 输出结果:\n0 1 2 3 4 5 6 7 8 9 10 进程已结束,退出代码0 break\ncontinue 语句 # 终止循环，跳到下一个循环\npublic static void main(String[] args) { for (int x = 0; x \u0026lt;= 11; x++) { if (x % 10 == 0) { System.out.println(\u0026#34;ZZZ\u0026#34;);//x被10整除的时候输出,并且跳出if。进入循环 continue; } System.out.println(x); } } 输出结果:\nZZZ 1 2 3 4 5 6 7 8 9 ZZZ 11 用户交互 Scanner # java.util.Scanner 是 Java5 的新特征，我们可以通过 Scanner 类来获取用户的输入。\n基本语法 # 首先用import来引用Scanner类库 (内裤)。\nimport java.util.Scanner ; //引用Scanner类库 通过 Scanner 类的 next() 与 nextLine() 方法获取输入的字符串，\n在读取前我们一般需要使用 hasNext 与 hasNextLine 判断是否还有输入的数据。\nScanner s = new Scanner(System.in); //Scanner 基本语法 使用例 # 输入什么，就输出什么. ( 使用scan.hasNext() 和 scan.next() 分别执行 检测和接受输入 )\nimport java.util.Scanner ; //引用Scanner类库 public class Demo1 { public static void main(String[] args) { Scanner scan = new Scanner(System.in); //创建Scanner来检测用户输入 System.out.println(\u0026#34;请输入\u0026#34;); if (scan.hasNext()){ //scan.hasNext（）判断是否输入 String input = scan.next() ; //用scan.next()来获取输入的内容 System.out.println(\u0026#34;输入的是\u0026#34; + input); //输出 } //Scanner属于IO流，不用的时候应该关掉,否则会一直占用资源 scan.close(); //停止运行 } } 输入什么，就输出什么. ( 使用scan.hasNextLine() 和 scan.nextLine() 分别执行 检测和接受输入 )\nimport java.util.Scanner ; //引用Scanner类库 public class Demo1 { public static void main(String[] args) { Scanner scan = new Scanner(System.in); //创建Scanner来检测用户输入 System.out.println(\u0026#34;请输入\u0026#34;); if (scan.hasNextLine()){ //scan,hasNextLine（）判断是否输入 String input = scan.nextLine() ; //用scan.nextLine()来获取输入的内容 System.out.println(\u0026#34;输入的是\u0026#34; + input); //输出 } //Scanner属于IO流，不用的时候应该关掉,否则会一直占用资源 scan.close(); //停止运行 } } next() 与 nextLine() 的区别 # next() :\n一定检测到有效字符后才能结束输入\n如果输入的有效字符之间由空白，如 Hello World，next() 方法会自动去掉\nnext() 以空白作为结束符或分割符\nnext() 不接受带有空格的字符串\nnextLine() :\n以Enter为结束符，nextLine() 方法返回的是输入回车之前的所有字符\nScanner进阶 # 输入一组数，计算平均值\npublic class Demo1 { public static void main(String[] args) { Scanner scan = new Scanner(System.in); //创建Scanner来检测用户输入 //和 double All = 0 ; //输入的数字数量 int sum = 0 ; //输入的数字 double a = 0 ; System.out.println(\u0026#34;输入\u0026#34;); while ( scan.hasNextDouble() ){ a = scan.nextDouble() ; sum++ ; All = All + a ; System.out.println(\u0026#34;已经输入了\u0026#34; + sum +\u0026#34;个数字,当前结果为\u0026#34; + All ); } System.out.println(\u0026#34;一共输入了\u0026#34; + sum +\u0026#34;个数字\u0026#34;); System.out.println(\u0026#34;平均值为\u0026#34; + All/sum); } } 方法 # 何为方法 # JAVA方法是语句的结合，组合在一起来实现某种功能。（可以理解为Python的函数）\n本质是功能块，是实现功能的语句块的有序集合\n方法包含在类和对象中 方法在程序中被创建，在其他地方被调用 在JAVA中，方法不能独立存在（必须在类里面建立方法）\n方法定义的原则：\n保持方法原子性(一个方法实现一个功能)\n方法的定义和调用 # **修饰符：**修饰符，这是可选的，告诉编译器如何调用该方法。定义了该方法的访问类型。\n返回值类型 ：方法可能会返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需的操作，但没有返回值。在这种情况下，returnValueType 是关键字void。\n**方法名：**是方法的实际名称。方法名和参数表共同构成方法签名。\n**参数类型：**参数像是一个占位符。当方法被调用时，传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的，方法可以不包含任何参数。\n**方法体：**方法体包含具体的语句，定义该方法的功能。\n例1\n/** 修饰符 返回值类型 方法名(参数类型 参数名){ ... 方法体 ... return 返回值; } */ public static void main(String[] args) { //main int add = add (1, 2) ; //给方法的参数赋值 System.out.println(add); //调用方法使用 } public static int add (int a, int b){ /* 修饰符public static 返回类型 int 方法名 add 数据类型是 int 数据名是 a , b */ int c = a + b ; return c ; //返回c 值 } 例2\npublic static void main(String[] args) { int add = add (1, 2) ; System.out.println( \u0026#34;add方法: \u0026#34; + add); N(0); } public static int add (int a, int b){ //整数加法方法 int c = a + b ; return c ; } public static int N (int a){ //循环输出数字方法，这里的a是形参 for (int b = 0 ; b \u0026lt;= 5 ; b++){ // 这里的b为实参 System.out.println(b); a = b ; //将实参的值赋给形参 } return a ; //返回a值 } 方法调用 # 对象名.方法名(实参列表)\n如果返回是个值，方法调用一般是一个值。\nint larger = max(30, 40); 如果是viod，返回值是个语句\nSystem.out.println(\u0026#34;Hello,World！\u0026#34;); 例如下\npublic class Demo { public static void main(String[] args) { int add = add (1, 2) ; System.out.println( \u0026#34;add方法: \u0026#34; + add); System.out.println( \u0026#34;N方法: \u0026#34; + N(0)); Text( 2) ; // Text() ; } public static int add (int a, int b){ ///add方法，返回值int int c = a + b ; return c ; } public static int N (int a){ for (int b = 0 ; b \u0026lt;= 2 ; b++){ //N方法，返回值int a = b ; } return a ; } public static void Text(int a ){ //Text方法,返回值void，不用加return if (a == 2){ System.out.println(\u0026#34;Text:\u0026#34; + a); } } // public static void Text(){ // System.out.println(\u0026#34;Text\u0026#34;); // } } 方法的重载 # 在一个类中方法名相同，但参数不一致的方法\n重载规则\n方法名相同 方法的参数类型，参数个不一样 方法的返回类型可以不相同 方法的修饰符可以不相同 main 方法也可以被重载 实例\npublic class Demo { public static void main(String[] args) { double a = N(1) ; int b = N(2) ; System.out.println(a); System.out.println(b); } //int public static int N (int a){ //方法N,返回值 int return a ; } //double public static double N (double c){ //方法N,返回值 double return c ; } } 可变参数 # 可以接受多个值的形参\n在方法声明中，指定参数类型要加省略号( \u0026hellip; ) 可变参数只能作为函数的最后一个参数，但其前面可以有也可以没有任何其他参数 public class Demo1 { public static void main(String[] args) { Demo1 Demo1 = new Demo1() ; //定义一个demo1 Demo1.Text(1,2,3); //调用Text方法 } public static void Text (int ...i){ System.out.println(i[0]); System.out.println(i[1]); System.out.println(i[2]); } } 递归 # 方法A调用方法A,自己调用自己\nint b = 0 ; public static void main(String[] args) { Text(1,0) ; } public static int Text (int i ,int b ){ if (b \u0026lt; 100){ System.out.println(i); b ++ ; } return Text(i,b) ; } 运行结果如下:\n对象之间相互引用，最终会导致栈溢出\n所以就需要加入边界条件了。\n递归结构包括两个部分：\n递归头：什么时候不调用自身方法，没有递归头，会陷入死循环\n递归体：什么时候需要调用自身方法\n所以之前的代码是没有递归头，有递归体。\npublic class Demo { public static void main(String[] args) { // Text(0) ; Text(9999); } public static int Text (int b ){ if (b \u0026lt;= 10){ //递归体 System.out.println(b); return 10 ; }else { return Text(b-1) ;//递归头 } } 修饰符 Static # static 修饰的变量 # 没加 static\npublic class MI{ int P = 10; //普通变量 P public void BB (){ System.out.println(P); } } 调用输出\npublic class iiuiu { public static void main(String[] args){ MI M1 = new MI(); M1.P = 100; //给实例 M1的变量 P 赋值 M1.BB(); MI M2 = new MI(); M2.BB(); } } 输出的结果：\n100 10 加入 static （在同一个类里面创建的所有对象共享一个值）\npublic class MI{ static int P = 10; //静态变量 P public void BB (){ System.out.println(P); } } 调用输出\npublic class Phone { public static void main(String[] args){ MI M1 = new MI(); M1.P = 100; M1.BB(); MI M2 = new MI(); M2.BB(); } } 输出结果:\n100 100 static 修饰的方法 # 用 static 修饰的方法，不属于某一个对象，属于某一个类。\npublic class House { static int A ; //静态变量A public static void DDD(){ //静态方法DDD A = 10; } } 在静态方法中只能使用静态变量，而普通方法中可使用普通变量和静态变量\n数组 # 数组是相同类型数据的有序集合 数组描述的是相同类型的若干的数据，按照一定的先后次序排列组合而成 其中，每个数据称作每个数组元素，每个数组元素可以通过一个下标来访问 数组的声明与创建 # 声明数组变量,才能在程序中使用数组\n数组里的元素是按照索引访问的，索引从零开始。\n声明数组变量 # /* 声明数组 dataType[] arrayRefVar; // 首选的方法 或 dataType arrayRefVar[]; // 效果相同，但不是首选方法 */ /* 创建数组并赋值 arrayRefVar = new dataType[arraySize]; arrayRefVar [] = value ; */ public class Demo { public static void main(String[] args) { int[] int1 ;//声明int数组，名为int1 int1 = new int [10] ;//创建有10个空间数组 int1[0] = 10 ;//给数组第一个数赋值10 System.out.println (\u0026#34;int1 [0]=\u0026#34; + int1 [0]);//输出 } } 输出结果为\nint1 [0]=10 给数组元素循环赋值 # public class Demo { public static void main(String[] args) { int[] int1 ;//声明int数组，名为int1 int1 = new int [10] ;//创建有10个空间数组 for (int i = 0 ;i \u0026lt;int1.length;i ++){ /*定义整数i,arrays.length获取数组长度*/ int1 [i] = i ; //将i循环赋值给数组对应的元素 System.out.println ( int1 [i]);//输出各个元素 } } } 初始化以及内存分析 # 内存分析 # 堆 存放New的对象和数组\n所有线程共享，不会存放别的对象引用\n栈 存放基本变量类型（会包含那个基本类型的具体数值）\n引用对象的变量（会保存这个引用在堆里面的具体地址）\n方法区 可以被所有线程共享\n包含了所有class和static变量\n三种初始化 # 静态初始化 int [] a = {1,2,3} ; //创建 + 赋值 (静态初始化) Man [] mans = {new Man(1,1),new Man(2,2)} ; 动态初始化 int [] a = new int[2] ; //包含默认初始化 (动态初始化,默认值为0) a [0] = 1 ; a [1] = 2 ; 数组的默认初始化 数组是引用类型，它的元素相当于实例变量，一旦定义将被默认初始化,默认值为0.\n数组的使用 # 基础使用 # 遍历数组中所有元素 public class Demo { public static void main(String[] args) { int[] a = {1,2,3,4,5} ; for (int b = 0 ;b\u0026lt;a.length;b++){ //遍历数组元素 System.out.println(a[b]); } } } 所有元素的和 public class Demo { public static void main(String[] args) { int d = 0 ; int[] a = {1,2,3,4,5} ; for (int c = 0 ;c \u0026lt;a.length; c++){ d = d + a[c] ; } System.out.println(\u0026#34;所有元素和 =\u0026#34; + d); } } 数组中元素最大值 public class Demo { public static void main(String[] args) { int[] a = {1,2,3,4,5} ; int Max = a[0] ; for (int e = 0 ; e\u0026lt;a.length ; e++){ if (Max \u0026lt; a[e]){ Max = a[e] ; } } System.out.println(\u0026#34;Max =\u0026#34; + Max); } } 进阶使用 # For - Each循环 JDK1.5以上出现的功能，相当于jAVA自己自带循环\npublic class Demo { public static void main(String[] args) { int[] a = {1,2,3,4,5} ; for (int b : a ){ //for - each遍历数组 System.out.println(b); } } } 输出数组的元素 public class Demo { public static void main(String[] args) { int[] a = {1,2,3,4,5}; Text(a) ; } public static void Text (int[] a){ for (int b = 0 ;b\u0026lt;a.length;b++){ //遍历数组元素 System.out.print(a[b] + \u0026#34;\\n\u0026#34;); } } } 反转数组 public class Demo { public static void main(String[] args) { int[] a = {1,2,3,4,5}; int[] II = TText(a) ; Text(II); } public static void Text (int[] a){ for (int b = 0 ;b\u0026lt;a.length;b++){ //遍历数组元素 System.out.print(a[b] + \u0026#34;\\n\u0026#34;); } } public static int[] TText (int[] a) { int[] G = new int[a.length] ; for (int Y = 0,U = a.length - 1;Y\u0026lt;a.length;Y++,U--){ G[U] = a[Y] ; } return G; } } 二维数组 # 每个元素都是一维数组的数组\nint[] [] a = new int [2] [5] ; int [] [] a = {{1,2,3,4,5},{6,7,8,9,0}} ; 遍历二维数组\npublic class Demo { public static void main(String[] args) { int[] [] a = {{1,2},{3,4},{5,6}}; for (int i = 0;i\u0026lt;a.length;i++){ //{1,2}{3,4}{5,6} for (int t =0;t\u0026lt;a[i].length;t++){ //1,2,3,4,5,6 System.out.println(a[i][t]); } } } } 泛型 # 泛型的本质就是\u0026quot;参数化类型\u0026rdquo;。一提到参数，最熟悉的就是定义方法的时候需要形参，调用方法的时候，需要传递实参。那\u0026quot;参数化类型\u0026quot;就是将原来具体的类型参数化.\n目的是为了避免强制格式转换带来的ClassCastException,类型转化异常。\n不使用泛型\nimport java.util.ArrayList; import java.util.List; public class Demo { public static void main(String[] args) { try { List list = new ArrayList(); list.add(\u0026#34;String\u0026#34;); list.add(123); for ( int i = 0;i \u0026lt; list.size();i++){ System.out.println((String) list.get(i)); //(String)强制转化 } }catch (ClassCastException classCastException){ System.out.println(classCastException); } } } 运行这段代码会发现出现了强制转换带来的ClassCastException 转化异常。\n就需要使用泛型来规范化\nimport java.util.ArrayList; import java.util.List; public class Demo { public static void main(String[] args) { List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); //使用泛型 list.add(\u0026#34;String\u0026#34;); list.add(123); for (int i = 0; i \u0026lt; list.size(); i++) { System.out.println((String) list.get(i)); } } } 排序算法 # 冒泡排序 # 比较数组中，两个相邻的元素，如果第一个数比第二个数大，我们就交换它的位置 每一次比较，都会产生一个最大，或者最小的数字 下一轮则可以减少一次排序 一次循环直到结束 代码实现 # public static int[] T(int[] a){ //名为T的冒泡排序 int Y = 0;//临时变量 for (int Q = 0;Q\u0026lt;a.length-1;Q++){ //列表多少个元素，循环次数的作用 for (int W = 0;W\u0026lt;a.length-1-Q;W++){ //每次循环减少，循环次数 if (a[W]\u0026gt;a[W+1]){//比大小 Y = a[W] ; //大的值赋给临时变量 a[W] = a[W+1] ; //小的值前移 a[W+1] = Y ; //大的值赋给后面 } } } return a ; } 完整代码 # public class Demo1 { public static void main(String[] args) { int [] a = {1,3,4,2,5,6,9,7} ; Text(T(a)) ; } public static int[] T(int[] a){ //名为T的冒泡排序 int Y = 0;//临时变量 for (int Q = 0;Q\u0026lt;a.length-1;Q++){ //列表多少个元素，循环次数的作用 for (int W = 0;W\u0026lt;a.length-1-Q;W++){ //每次循环减少，循环次数 if (a[W]\u0026gt;a[W+1]){//比大小 Y = a[W] ; //大的值赋给临时变量 a[W] = a[W+1] ; //小的值前移 a[W+1] = Y ; //大的值赋给后面 } } } return a ; } public static void Text (int[] a){ for (int b = 0 ;b\u0026lt;a.length;b++){ //遍历数组元素显示 System.out.print(a[b] + \u0026#34;\\n\u0026#34;); } } } 稀疏数组 # 定义 # 当一个数组中大部分的元素为0，或为同一个值的数组时，可以使用稀疏数组来保存该数组。\n代码 # 打印输出二维数组\npublic class Demo1 { public static void main(String[] args) { /** 创建一个原始的二维数组(11行11列) 0:表示没有棋子 1:表示黑子 2:表示白子*/ int Arrays[][] = new int[11][11]; Arrays[10][2] = 1; Arrays[3][3] = 2; System.out.println(\u0026#34;最初的二维数组为:\u0026#34;); int Value = 0; for (int row = 0;row \u0026lt; Arrays.length;row++) {//循环行（遍历） for (int column =0;column \u0026lt; Arrays[row].length;column++) {//循环列（遍历） System.out.printf(\u0026#34;%d\\t\u0026#34;, Arrays[row][column]);//遍历数组输出 , %d表示转为十进制数，\\t是制表符 Value ++ ; } System.out.println(); } System.out.println(\u0026#34;=================================\u0026#34;); int[][] Array1 =new int[Value+1][3]; int Time = 0 ; for (int row = 0;row \u0026lt; Arrays.length;row++) {//循环行（遍历） for (int column = 0; column \u0026lt; Arrays[row].length; column++){//循环列（遍历） if(Arrays[row][column] != 0){ Array1[Time][0] = row; Array1[Time][1]= column; Array1[Time][2] = Arrays[row][column]; Time ++ ; } } } System.out.println(\u0026#34;稀疏数组\u0026#34;); Array1[3][0] = 11; Array1[3][1] = 11 ; Array1[3][2] = 2 ; for (int i = 0;i\u0026lt;Array1[i].length;i++){ System.out.println(Array1[i][0] + \u0026#34;\\t\u0026#34;+ Array1[i][2] + \u0026#34;\\t\u0026#34; + Array1[i][2] + \u0026#34;\\t\u0026#34;); } } } 输出结果：\n最初的二维数组为: 0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t2\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t1\t0\t0\t0\t0\t0\t0\t0\t0\t================================= 稀疏数组 3\t2\t2\t10\t1\t1\t0\t0\t0\t面向对象 # 类的定义 # /** [修饰符]class 类名{ //成员变量 //构造方法 //成员方法 } */ 成员变量 # /** [修饰符]变量类型 变量名字 [= 默认值] ; 其中\u0026#34;修饰符\u0026#34;和\u0026#34;默认值\u0026#34;不是必须的。 修饰符有: [public protexted private] [static](static只执行一次) [final] 定义在类里面 */ int A ; int B = 10 ; public int C ; protected int D ; private int E ; static int F ; final int G ; 定义成员变量 # public class Demo { int A = 10; //成员变量 A public static void main(String[] args) { Demo Demo = new Demo(); } public Demo() { //构造方法Demo System.out.println(\u0026#34;Hello,World!\u0026#34;); } } 构造方法 # 和类名相同，自动调用\npublic class Demo { public static void main(String[] args) { Demo Demo = new Demo(); } public Demo() { //构造方法Demo System.out.println(\u0026#34;Hello,World!\u0026#34;); } public Demo(int R,int E){ System.out.println(\u0026#34;使用两个参数的构造方法\u0026#34;); } } 成员方法 # /** [修饰符]返回值类型 方法名{ 语句 } */ public int NP(int G){ return G; } 构造方法重载 # 两个方法方法名相同，但是参数列表不同，称为重载\npublic Demo(double U){ System.out.println(\u0026#34;使用构造方法\u0026#34;); } public Demo(int Y){ System.out.println(\u0026#34;使用构造方法重载\u0026#34;); } 调用以上方法\npublic class PPP {//新建PPP的类 public static void main(String[] args) { AAA N = new AAA(1.2);//将带有成员方法和构造方法的类实例化 System.out.println(\u0026#34;成员方法\u0026#34; + N.NP(8)); //成员方法调用 AAA N1 = new AAA(1);//调用 AAA N2 = new AAA(7,4); } } public class AAA { public int NP(int G){ //成员方法 return G; } public AAA(double U){ //构造方法 System.out.println(\u0026#34;使用构造方法\u0026#34;); } public AAA(int Y){ //构造方法 System.out.println(\u0026#34;使用构造方法重载\u0026#34;); } public AAA(int R,int E){ //构造方法 System.out.println(\u0026#34;使用两个参数的构造方法\u0026#34;); } } 输出结果:\n使用构造方法 成员方法8 使用构造方法重载 使用两个参数的构造方法 This 关键字 # 在类的方法中引用自身\nStudent类代码\npublic class Student { String name = \u0026#34;\u0026#34;; int age ; double Math ; double Chinese ; double English ; public Student(String name,int age,int English,int Math,int Chinese){ this.name = name; this.age = age ; this.English = English ; this.Math = Math ; this.Chinese = Chinese ; } public void printStudentInfo(){ System.out.println(\u0026#34;姓名：\u0026#34; + this.name + \u0026#34; 年龄:\u0026#34;+ this.age + \u0026#34; 各科成绩:\u0026#34; + \u0026#34;语文:\u0026#34; + this.Chinese + \u0026#34;数学:\u0026#34; + this.Math + \u0026#34;英语:\u0026#34; + this.English ); } public void printStudentInfo(String E , int A){ System.out.println(\u0026#34;姓名：\u0026#34; + name + \u0026#34; 年龄：\u0026#34;+ age); } } 调用Student代码\npublic class TestStudent { public static void main(String[] args) { Student Student1 = new Student(\u0026#34;\u0026#34;,0,0,0,0); Student1.name = \u0026#34;哈吉\u0026#34;; Student1.age = 12; Student1.English = 123; Student1.Math = 123; Student1.Chinese=123; Student1.printStudentInfo(\u0026#34;123\u0026#34;,123); Student1.printStudentInfo(); System.out.println(\u0026#34;===================\u0026#34;); Student Student2 = new Student(\u0026#34;张天\u0026#34;,12,92,86,87); Student2.printStudentInfo(); Student2.printStudentInfo(\u0026#34;张天\u0026#34;,12); } } 方法的参数传递 # 当初参数是基本数据类型的时候，经过方法输出后参数不变，如果是引用数据类型，会改变\n//引用数据类型当参数 public class Demo { public static void main(String[] args) { int[] AA = {50,100}; Data(AA); System.out.println(\u0026#34;AA[0]= \u0026#34; + AA[0] + \u0026#34;, AA[1]= \u0026#34; + AA[1]); } public static void Data(int[] BB){ int AP = BB[0]; BB[0] = BB[1]; BB[1] = AP; System.out.println(\u0026#34;BB[0]= \u0026#34;+ BB[0] + \u0026#34; ,BB[1]= \u0026#34; + BB[1]); } } 输出结果:\nBB[0]= 100 ,BB[1]= 50 AA[0]= 100, AA[1]= 50 //基本数据类型当参数 public class Demo { public static void main(String[] args) { int a = 100; int b = 50; Data(a,b); System.out.println(\u0026#34;a= \u0026#34; + a + \u0026#34;, b= \u0026#34; + b); } public static void Data(int x,int y){ int AP = x; x = y; y = AP; System.out.println(\u0026#34;x= \u0026#34;+ x + \u0026#34; ,y= \u0026#34; + y); } } 输出结果:\nx= 50 ,y= 100 a= 100, b= 50 可变长参数\npublic class GongSi { //输出 public void BuMen (String A,String...B){ //String... 为可变长参数，需要遍历输出，String为数据类型 System.out.println(\u0026#34;部门为 \u0026#34; + A); for (int i =0; i \u0026lt;B.length;i++){ System.out.println(\u0026#34;人员为 \u0026#34; + B[i]); } } } public class Demo { // 赋值输出 public static void main(String[] args) { GongSi GongSi = new GongSi(); GongSi.BuMen(\u0026#34;123不mean\u0026#34;,\u0026#34;123123123人\u0026#34;,\u0026#34;123123啊的时间弄多久哦爱仕达\u0026#34;); } } 隐藏,封装和继承 # 封装 # 封装是面向对象的三大特征之一，将对象的状态信息隐藏在内部，不允许外部程序直接访问对象。\nPrivate关键字 # 在变量前加 private 可以封装变量，封装后只能在同一个类中修改。\npublic class Demo1 { private int G = 10; //封装变量G } public class Demo { public static void main(String[] args) { Demo1.G = 100; //修改变量G，这时会报错 } } 报错提示:\njava: G 在 Demo2.Demo1 中是 private 访问控制 要使用方法来赋值:\nclass Demo1 { private int G = 10; public int SetG(int G){ //在原来的类中写方法 this.G = G ; return this.G; } } public class Demo { public static void main(String[] args) { Demo1 demo1 = new Demo1(); int P = demo1.SetG(1000); //通过方法赋值 System.out.println(P); } } 继承 extends # 继承是面向对象的三大特征之一\n//[修饰符] class 子类名 extends 父类名(){} 重写父类方法\n直接添加方法\n//父类 class Person { String name ; int age; public Person(){ } public Person(String name ,int age){ this.name = name; this.age = age; } public void say(){ System.out.println(name + \u0026#34; say,I`m \u0026#34; + age +\u0026#34; years old\u0026#34; ); } } //子类 public class Person1 extends Person{ public void say(){ //这里重写了say的无参方法 System.out.println(name + \u0026#34; no say \u0026#34; ); } public static void main(String[] args) { Person1 Qin = new Person1(); Qin.name =\u0026#34;Qin\u0026#34;; Qin.say(); } } 输出结果:\nQin no say 这样做的问题是会导致继承的类中方法无法使用,就要用super来代替父类调用:\nclass Person { String name ; int age; public Person(){ } public Person(String name ,int age){ this.name = name; this.age = age; } public void say(){ System.out.println(name + \u0026#34; say,I`m \u0026#34; + age +\u0026#34; years old\u0026#34; ); } } //子类 public class Person1 extends Person{ //子类 public void say(){ super.say();//用super来调用父类的say方法 System.out.println(name + \u0026#34; no say \u0026#34; ); } public static void main(String[] args) { Person1 Qin = new Person1(); Qin.name =\u0026#34;Qin\u0026#34;; Qin.say(); } } 输出结果:\nQin say,I`m 0 years old Qin no say 如果父类中有构造方法，子类中必须有构造方法，而且必须调用\n//使用super继承父类的构造方法 //子类 class Trees extends Tree{ int number ; public static void main(String[] args) { Trees Trees = new Trees(); Trees.number = 100 ; Trees.flower(); } public Trees (){ System.out.println(\u0026#34;子类无参构造\u0026#34;); } //如果父类中有不带参数的构造方法，子类会自动调用 public Trees (String name ,String kind,int number){ super(name,kind); //注意:父类的构造方法必须要在子类构造方法的第一行 this.number = number; } public void flower(){ System.out.println(\u0026#34;这是 \u0026#34; + kind + \u0026#34; ,名为 \u0026#34; +name + \u0026#34; ,一共\u0026#34; + number + \u0026#34;棵\u0026#34;); } } //父类 public class Tree { String name = \u0026#34;桃树\u0026#34;; String kind = \u0026#34;果树\u0026#34;; public Tree(){ System.out.println(\u0026#34;父类无参构造\u0026#34;); } public Tree(String name ,String kind){ this.name = name ; this.kind = kind ; } public void flower(){ System.out.println(\u0026#34;这是 \u0026#34; + kind + \u0026#34; ,名为 \u0026#34; +name + \u0026#34; .\u0026#34;); } } 输出结果:\n父类无参构造 子类无参构造 这是 果树 ,名为 桃树 ,一共100棵 多态 # 指同样的操作作用于不同的对象，可以有不同的解释，产生不同的执行结果.\n向上转型 # 父类引用指向子类对象为向上转型，语法格式如下：\nfatherClass obj = new sonClass(); 向上转型就是把子类对象直接赋给父类引用，不用强制转换。使用向上转型可以调用父类类型中的所有成员，不能调用子类类型中特有成员，最终运行效果看子类的具体实现。\npackage Demo2; class Animal{ String Name = \u0026#34;Aninal\u0026#34; ; String Say = \u0026#34;SAYYY\u0026#34;; public Animal(String name, String say) { this.Name = name; this.Say = say; } public Animal() { } public void Say(){ System.out.println( \u0026#34; Name: \u0026#34; + Name + \u0026#34; Say: \u0026#34; + Say); } } class Dogs extends Animal{ public void Say(){ System.out.println( \u0026#34; Say: \u0026#34; + Say + \u0026#34; Name: \u0026#34; + Name ); } public void Run() { System.out.println(Name + \u0026#34;Run\u0026#34;); } } public class Test222 { public static void main(String[] args) { Animal animal = new Dogs(); //向上转型 animal.Say(); animal.Run(); } } 这里会看到报错:\njava: 找不到符号 符号: 方法 Run() 位置: 类型为Demo2.Animal1的变量 Dog1 向下转型 # 与向上转型相反，子类对象指向父类引用为向下转型，语法格式如下：\nsonClass obj = (sonClass) fatherClass; 向下转型可以调用子类类型中所有的成员，不过需要注意的是如果父类引用对象指向的是子类对象.\nclass Animal{ String Name = \u0026#34;Aninal\u0026#34; ; String Say = \u0026#34;SAYYY\u0026#34;; public Animal(String name, String say) { this.Name = name; this.Say = say; } public Animal() { } public void Say(){ System.out.println( \u0026#34; Name: \u0026#34; + Name + \u0026#34; Say: \u0026#34; + Say); } } class Dogs extends Animal{ public void Say(){ System.out.println( \u0026#34; Say: \u0026#34; + Say + \u0026#34; Name: \u0026#34; + Name ); } public void Run() { System.out.println(Name + \u0026#34;Run\u0026#34;); } public static void main(String[] args) { Animal animal = new Dogs(); // 向上转型 Dogs dogs = (Dogs) animal; //向下转型 dogs.Say(); dogs.Run(); } } 输出结果：\nSay: SAYYY Name: Aninal AninalRun 初始化块 # 初始化块\n作用类似构造方法，可以对对象进行初始化操作。 无法传入任何参数，可以理解为提前执行的代码 使用初始化块\n//class 类名{ //[修饰符]{ // 初始化块的可执行代码 // } //} class Demo{ { System.out.println(\u0026#34;初始化块\u0026#34;); } public Demo(){ System.out.println(\u0026#34;构造方法\u0026#34;); } } public class Demo1 { public static void main(String[] args) { Demo Demo = new Demo(); } } 注意: 如果有静态初始化块，会优先执行\nclass Demo1{ static { //属于类 System.out.println(\u0026#34;静态初始化块\u0026#34;); } { //属于对象 在初始化块中的变量属于局部变量，需要初始化 System.out.println(\u0026#34;初始化块\u0026#34;); } public Demo1(){ //属于对象 System.out.println(\u0026#34;构造方法\u0026#34;); } } public class Demo { public static void main(String[] args) { Demo1 Demo = new Demo1(); } } 运行结果：\n静态初始化块 初始化块 构造方法 Final 修饰符 # final 关键字可用于修饰类，变量和方法。用来表示修饰的变量不可改变。\n一旦初始化就无法赋值了。\n\\[warning\\]final 只能在定义时，构造方法里以及初始化块中，无法在Main方法中赋值\n\\[/warning\\] Final 修饰变量 定义必须初始化，一旦初始化就无法赋值\nclass Demo{ final static int A ; final int B ; final int C = 10 ; //定义 static { //静态初始化块 A = 10 ; } public Demo (){ //构造方法 B = 10 ; } public int getA(){ return A ; } public int getB(){ return B ; } public int getC(){ return C; } } public class Demo1 { public static void main(String[] args) { Demo Demo = new Demo(); System.out.println(Demo.getA()); System.out.println(Demo.getB()); System.out.println(Demo.getC()); } } Final修饰数组 指向的地址无法改变，可以改变数组中元素的值\npublic class Demo { public static void main(String[] args) { final int[] Arr = {1,2,3}; //定义数组Arr 元素有 1,2,3 for (int i = 0;i\u0026lt;Arr.length;i++){ Arr[i] = Arr[i] * 2 ; //元素大小乘2 System.out.print(Arr[i] +\u0026#34; ,\u0026#34; ); //遍历输出 } } } 输出结果：\n2 ,4 ,6 , Final 修饰方法 如果有子类，则无法对加 Final 的方法进行重写\nFinal 修饰类 将无法被继承\n抽象类(abstract ) # 不包含方法体的方法称为 “抽象方法”，（在JAVA中只要有花括号括起来的都是方法体）\n作用＆注意 拿来当父类使用，无法被实例化，abstract方法只能存在于abstract类中,abstract方法只能被重写\n无法修饰属性,无法修饰构造方法，abstract只有重写才有意义\nabstract class DemoAbstract { public abstract void Print1(); //抽象方法 Print1 public void Print2(){ //普通方法 Print2 System.out.println(\u0026#34;DemoAbstract.Print2\u0026#34;); } } public class Demo extends DemoAbstract{ public void Print1(){ //重写抽象方法 System.out.println(\u0026#34;Demo.Print\u0026#34;); } public static void main(String[] args) { Demo Demo = new Demo(); Demo.Print1(); //继承重写的抽象方法 Demo.Print2(); //抽象类写的普通方法 } } abstract无法和final同时使用\nabstract 构造方法\nabstract class DemoAbstract { String Name ; public DemoAbstract(String Name){ this.Name = Name; //抽象类中的构造方法 DemoAbstract } } public class Demo extends DemoAbstract{ public Demo(String Name){ //子类的构造方法 super(Name); //super调用父类 DemoAbstract 的构造方法 } public static void main(String[] args) { Demo Demo = new Demo(\u0026#34;Name\u0026#34;); System.out.println(Demo.Name); //输出父类 DemoAbstract 的属性 Name 的值 } } 接口 # JAVA通过接口来实现多继承\n定义接口 # public interface interface1 { //public intaerfasce 接口名 //变量全是静态变量 public void Print(); //接口中的方法默认都是抽象方法(public abstract), public abstract可以省略 } 实现(继承)接口时，使用implements关键字来实现，而不是extends。\n而接口继承接口时，使用extends\ninterface interface1 { public void PrintInterFace(); } interface interface2 extends interface1{ //extends继承接口 public void PrintInterFace2(); } abstract class TestClass{ public void PrintClass(){ System.out.println(\u0026#34;PrintClass\u0026#34;); } } public class TestInterface extends TestClass implements interface1,interface2{ @Override public void PrintInterFace() { //重写接口1的PrintInterFace静态方法 System.out.println(\u0026#34;PrintInterFace1\u0026#34;); } @Override public void PrintInterFace2() { //重写接口2的PrintInterFace2静态方法 System.out.println(\u0026#34;PrintInterFace2\u0026#34;); } @Override public void PrintClass() { //重写抽象类的抽象方法 super.PrintClass(); } public static void main(String[] args) { TestInterface TestInterface = new TestInterface(); TestInterface.PrintInterFace(); TestInterface.PrintInterFace2(); TestInterface.PrintClass(); } } 内部类 # 内部类可以直接使用父类的私有(private)变量,而继承无法使用。\nclass OutClass{ //外部类 private int A = 10 ; class InClass{ //内部类 public void PrintIn(){ //内部类方法 System.out.println(A+10); } } static class StatiInClass{ //内部静态类 public static void PrintStatic(){ //内部静态类方法 System.out.println(\u0026#34;StaticInClass\u0026#34;); } } public void PrintOut(){ //外部类方法 System.out.println(A); } } public class Demo { public static void main(String[] args) { OutClass OC = new OutClass(); OC.PrintOut(); //外部类 PrintOut() OutClass.InClass IC = OC.new InClass(); //通过外部类对象来定义内部类的对象 //格式为 ： 外部类名.内部类名 对象名 = 外部类对象 . new 内部类（） ； IC.PrintIn(); //内部类 PrintIn() //直接使用 外部类名.内部类名 对象名 = new 外部类名.内部类名(); 来实例化 OutClass.StatiInClass SC= new OutClass.StatiInClass(); SC.PrintStatic(); } } 要在内部类方法中调用外部类中的值，使用 外部类名.this.变量名 来实现。\nAPI # 可以直接调用的方法集合\npublic static void main(String[] args) { String S1 = \u0026#34;HEKHHKjpg\u0026#34;; String S2 = \u0026#34;123123\u0026#34;; String S3 = S1.concat(S2); //使用concat连接字符串 System.out.println(S3); System.out.println(\u0026#34;=================1. replace===================\u0026#34;); //replace来替换字符串 String S4 = S2.replace(\u0026#39;2\u0026#39;,\u0026#39;R\u0026#39;);//使用API将所有的2替换为R System.out.println(S4); System.out.println(\u0026#34;===================2.substring=================\u0026#34;); //substring截取字符串 String S5 = S1.substring(1,3);//去索引值，包含开不包含最后一个\u0026#39; String S6 = S2.substring(1); // System.out.println(S5); System.out.println(S6); System.out.println(\u0026#34;====================================\u0026#34;); //toLowerCase将所有字符转为小写,toUpperCase转为大写 String S7 = S1.toLowerCase(); String S8 = \u0026#34;lllllll\u0026#34;; String S9 = S8.toUpperCase(); System.out.println(S7); System.out.println(S9); System.out.println(\u0026#34;====================================\u0026#34;); //trim将首尾的空格去掉 String S10 = \u0026#34; HE EH \u0026#34;; System.out.println(\u0026#34;\u0026#34; + S10 + \u0026#34;\u0026#34;); String S11 = S10.trim(); System.out.println(\u0026#34;\u0026#34; + S11+ \u0026#34;\u0026#34;); System.out.println(\u0026#34;====================================\u0026#34;); //charAt返回固定位置的字符 char S12 = S1.charAt(1); System.out.println(S12); System.out.println(\u0026#34;====================================\u0026#34;); //endsWith判断末尾是否为某字段结尾 boolean B1 = S1.endsWith(\u0026#34;.jpg\u0026#34;); System.out.println(B1); System.out.println(\u0026#34;================indexOf====================\u0026#34;); //indexOf判断字符或者字符串再当前字符串中的位置 int I1 = S1.indexOf(\u0026#34;.jpg\u0026#34;); System.out.println(I1); System.out.println(\u0026#34;===============length=====================\u0026#34;); //length判断字符长度 int I2 = S1.length(); System.out.println(I2); System.out.println(\u0026#34;===============equals=====================\u0026#34;); //equals判断两个字符串是否相等 boolean B2 = S1.equals(S2); boolean B3 = S1.equals(S1); boolean B4 = S1.equals(S1.toLowerCase()); System.out.println(\u0026#34;S1.equals(S2)\u0026#34; + B2); System.out.println(\u0026#34;S1.equals(S1)\u0026#34; + B3); System.out.println(\u0026#34;S1.equals(S1.toLowerCase())\u0026#34; + B4); System.out.println(\u0026#34;===============split=====================\u0026#34;); //split分割字符串 String[] SP13 = S1.split(\u0026#34;K\u0026#34;); //遇到K分割，返回数组 for (int i = 0;i \u0026lt; SP13.length;i++){ System.out.println(SP13[i]); } System.out.println(\u0026#34;====================================\u0026#34;); //ceil向上取整 ,floor向下取整,round四舍五入 double D1 = -3.1415926; double D2 = Math.ceil(D1); System.out.println(D2); System.out.println(\u0026#34;==================Max==================\u0026#34;); //Max最大,Min最小,Abs绝对值 double D3 = Math.max(1,2); double D4 = Math.min(1,2); double D5 = Math.abs(-12);//绝对值 System.out.println(D3); System.out.println(D4); System.out.println(\u0026#34;=================三角函数===================\u0026#34;); //正弦 double DS1 = Math.sin(Math.PI/6); //弧度制表示角度 //余弦 double DS2 = Math.cos(Math.PI/6); //正切 double DS3 = Math.tan(Math.PI/6); System.out.println(\u0026#34;Math.sin(Math.PI): \u0026#34; + DS1 + \u0026#34;\\n Math.cos(Math.PI): \u0026#34; + DS2 + \u0026#34;\\n Math.tan(Math.PI): \u0026#34; + DS3); System.out.println(\u0026#34;=================幂运算===================\u0026#34;); //pow幂运算 sqrt平方根 log10以10为底的对数 double D6 = Math.pow(3,4); //三的四次方 double D7 = Math.sqrt(25); double D8 = Math.log10(233); System.out.println(\u0026#34;=================随机数===================\u0026#34;); //random 获得随机数 double D9 = Math.random(); } 异常 # 异常是指程序运行当中出现的不期而至的情况。在程序运行期间，异常会影响正常程序的运行流程。\n异常的分类 # 检查性异常 最具代表的时用户在错误或者问题的时候发生的，比如打开一个不存在的文件等\n运行时异常 运行时异常是容易被忽略的异常，运行时可以忽略\n错误ERROR 错误不是异常，而是摆脱控制的问题。在代码中常常会发生。比如栈溢出等。\n异常处理机制 # 捕获异常\ntry - catch - finally\ntry{ } 里写入包含异常的代码，catch(){ }中写入捕获的异常\npublic static void main(String[] args){ try { //包含异常的代码 int A = (int) Math.random(); System.out.println(\u0026#34;A: \u0026#34; + A); int B = (int) Math.random(); System.out.println(\u0026#34;B: \u0026#34; + B); int C = A/B; }catch(ArithmeticException EX){ //捕获异常 括号内捕获的异常要和try中异常相同 System.out.println(EX); //显示异常 } } 输出结果:\nA: 0 B: 0 java.lang.ArithmeticException: / by zero 如果要使用catch(){ }来捕获两个异常，catch则会捕获第一个匹配到的异常\npublic class Demo { public static void main(String[] args) { try { int A = (int) Math.random(); System.out.println(\u0026#34;A: \u0026#34; + A); int B = (int) Math.random(); System.out.println(\u0026#34;B: \u0026#34; + B); int C = A / B; //除数为零异常 int[] IA = {1, 2, 3, 4, 5}; System.out.println(IA[5]); //数组下标越界异常 } catch (ArithmeticException AE) { //捕获除数为零异常 System.out.println(AE); } catch (ArrayIndexOutOfBoundsException AIOOE) { //捕获数组下标越界异常 System.out.println(AIOOE); } } } 运行结果:\nA: 0 B: 0 java.lang.ArithmeticException: / by zero finally{ }的作用是无论是否发生异常，都会执行\npublic static void main(String[] args){ try { int[] IA = {1,2,3,4,5}; }catch(Exception A){ System.out.println(A); }finally { System.out.println(\u0026#34;finally\u0026#34;); } } 运行结果:\nfinally ","date":"2022 年 03 月 04 日","externalUrl":null,"permalink":"/posts/java%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80/","section":"Posts","summary":"","title":"Java语言基础","type":"posts"},{"content":"假期闲来无事，与其虚度，不如做点事干 你信吗？我不信 (￣ε(#￣)☆╰╮o(￣皿￣///)。于是就买了个开发板，正好借此学一学C语言。\n于是，就立马买了需要的东西，清单如下所示：\n物品 价格 数量 备注 Esp32 DevKit v1 30pin开发板 21.5 1 有30pin和36pin的版本 40P彩排杜邦线 2.81 1 母对母 1.8寸 彩色TFT屏幕 20 1 不带触摸 开发板连接线 0 0 家里找的 *注意:我买的屏幕型号是MSP1803，需要连接背光才能显示 *\n一共44.31块，还行吧。\n之后就找了个做小电视的视频就开始学了。\n然后我懵了，我没学过单片机，大一只学过Python和HTML入门，我看不懂商家给的资料，也看不懂UP给的教程。\n然后鼓捣了好久。玩了好久，才想起来有个单片机。\n现在才整的有些样子，现在基本完成了，那么我就把我一路上踩过的坑说一下。\n获取Chip ID报错 # 报错显示 “ exec: \u0026lsquo;cmd\u0026rsquo;: executable file not found in %PATH% 为开发板 ESP32 Dev Module 编译时出错。”\n提示没有配置环境变量，配置完成，问题解决。\n屏幕显示问题 # 屏幕不显示 # 检查自己的屏幕，教程里面是不接背光的，可以烧录进去，然后接背光试试。我今天(2022.2.12)才发现我的屏幕是一定要接背光的，我还搞了好久。\n图片显示不正常，且有小部分花屏 # 有的人可能烧录进去以后发现屏幕是这个样子：\nhttps://xenolies-blog-images.oss-cn-hangzhou.aliyuncs.com/Pics/IMG20220212145658-768x1024.webp\n图片显示非常不正常，还有花屏的情况。\n根据这个帖子的说法，是设计问题，可以通过切换配置文件的参数解决 。解决方法来自这个帖子。\n按照这样的路径（.\\arduino\\arduino-nightly\\hardware\\espressif\\esp32\\libraries\\TFT_eSPI）打开TFT_eSPI,找到User_Setup.h，打开。\n找到这样的地方，并且把原来的#define ST7735_GREENTAB2注释掉，把#define ST7735_REDTAB解除注释，就显示正常了。\n此方法选择的参数有可能其他屏幕无效，可以在几个参数切换试试看\n然后重新烧录进去程序。\n这样就显示正常了。\n色彩显示错误 # 烧录进去了程序，有可能出现这样的情况：本该显示蓝色的地方显示了黄色。这是因为UP的程序里使用的是BGR,而你的屏幕是RGB,这样导致的色彩问题。\n解决办法：\n按照这样的路径（.\\arduino\\arduino-nightly\\hardware\\espressif\\esp32\\libraries\\TFT_eSPI）打开TFT_eSPI,找到User_Setup.h，打开。\n找到#define TFT_RGB_ORDER TFT_BGR 这样的一行，将其注释掉，启用上面的define TFT_RGB_ORDER TFT_RGB。\n完成以后就是这样子\n显示不全 # UP的代码烧录进去以后会发现有个问题，就是在天气页面，如果显示三个字的地名或者显示风向为“东北风‘的时候会显示不全，这时候就要调整一下显示的位置：\n我这里把调好的放在下面\n/*******************天气界面显示****************/ void show_weather(uint16_t fg,uint16_t bg) { tft.setSwapBytes(true); //使图片颜色由RGB-\u0026gt;BGR tft.pushImage(0, 0, 64, 64, weather[ph]); showMyFonts(75, 25, now_address.c_str(), TFT_WHITE);//因为图片大小的限制，如果是四个字的地名的话需要调整图片大小，还有本行调小X轴数值（数字75所在的位置） showMyFonts(75, 45, now_wea, TFT_WHITE);//天气 tft.pushImage(0 , 65, 30, 30, temIcon); tft.pushImage(0, 95, 30, 30, humIcon);;//湿度 tft.pushImage(50, 65, 30, 30, rainIcon);//雨 tft.pushImage(45, 95, 30, 30, windIcon);//风 showtext(25,75,1,1,fg,bg,now_high_tem + \u0026#34;/\u0026#34; + now_low_tem);//低温高温 showtext(25,75,1,1,fg,bg,now_high_tem + \u0026#34;/\u0026#34; + now_low_tem);//低温高温 showtext(85,75,1,1,fg,bg,now_rainfall +\u0026#34;mm\u0026#34;);//降水量 showtext(25,105,1,1,fg,bg,now_hum+\u0026#34;%\u0026#34;);//湿度 String now_wind = now_wind_direction + \u0026#34;风\u0026#34;;//什么风 showMyFonts(75, 100, now_wind.c_str(), TFT_WHITE); get_weather();// 再次运行获取天气的函数，保证天气更新 } 接线问题 # 有的屏幕的A0引脚接那里？ # 可以接到DC的引脚那里，性质相同 ，都是数据选择功能\n其他的引脚接哪里？以我的为例。\n屏幕引脚标注 开发板引脚标注 功能 VCC VIN 正极 GND GND 负极 CS D27 片选信号 RESET D26 复位信号 A0(DC) D25 寄存器/数据选择信号 SDA D23 SPI 总线写数据信号 SCK D18 SPI 总线时钟信号 LED 3V3 背光 其他 # 1.8寸的TFT屏幕，型号是MSP1803的相关的资料：\nhttp://www.lcdwiki.com/res/MSP1803/1.8inch_SPI_Module_MSP1803_User_Manual_CN.pdf\n30引脚的DOIT Esp32 Dev Kit v1开发板的引脚图,可以参考下：\nhttps://www.electronicshub.org/esp32-pinout/\nEsp32 DevKit v1的30引脚和36引脚的对比：\nhttps://cloud.tencent.com/developer/article/1749032\n","date":"2022 年 02 月 12 日","externalUrl":null,"permalink":"/posts/esp32%E5%88%B6%E4%BD%9C%E5%B0%8F%E7%94%B5%E8%A7%86%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","section":"Posts","summary":"","title":"esp32制作小电视学习笔记","type":"posts"},{"content":"","date":"2022 年 02 月 12 日","externalUrl":null,"permalink":"/tags/%E5%8D%95%E7%89%87%E6%9C%BA/","section":"Tags","summary":"","title":"单片机","type":"tags"},{"content":"Pyinstaller是可以把Python工程打包成为exe的一个Python模块。网上教程有很多，但是我在使用的时候会遇到很多问题，进而搜索大量网页，非常麻烦。所以准备写一篇文章把我碰到的一些问题整合起来，这样的话免去去查找大量网站的烦恼。\n1.如何安装Pyinstaller # 最简单的方法就是打开Win+R输入cmd，打开cmd窗口输入\npip install pyinstaller 但是，\n有的时候你安装好了Python，用pip安装的时候会出现这个问题：\n这个就说明Python的环境变量没有配置好，需要配置下，这时候需要找到Python安装目录下的Scripts文件夹，像我的就是C:\\Users\\111\\AppData\\Local\\Programs\\Python\\Python311\\Scripts\n确认文件夹下有没有pip.exe,如下图：\n如果有的话，把文件夹路径复制下来，然后就去“环境变量”那里找到path变量，添加地址保存即可\n配置完成后记得重新打开CMD安装窗\n如何找到环境变量，请在这个帖子查看。\n如果出现这个了，说明正在安装了\n最后显示这个，就说明成功安装了\n如果出现这个了，说明要升级pip\n直接输入下面这个就可以了。\npython -m pip install --upgrade pip 2.用Pyinstaller打包 # Pyinstaller主要语法就是：Pyinstaller +指令+Python文件\n下面就是些常用的指令：\n-F，-onefile 产生单个的可执行文件 -D，\u0026ndash;onedir 产生一个目录（包含多个文件）作为可执行程序 -a，\u0026ndash;ascii 不包含 Unicode 字符集支持 -d，\u0026ndash;debug 产生 debug 版本的可执行文件 -w，\u0026ndash;windowed，\u0026ndash;noconsolc 指定程序运行时不显示命令行窗口（仅对 Windows 有效） -c，\u0026ndash;nowindowed，\u0026ndash;console 指定使用命令行窗口运行程序（仅对 Windows 有效） 上表来源\n然后就可以在cmd窗口输入指令来打包了，打包的时候记得转到文件所在的文件夹\n转到要打包的Python所在的文件夹,可以使用cd+空格+文件夹地址来实现\n这样就说明打包成功了\n其中这个\n是最后exe文件输出的地址\n还要这个\n说明已经打包成为exe文件了\n之后就可以使用了，不需要再搭建Python环境了\n如果打包完成发现窗口一闪而过，可以试试在代码末尾加一个input()\n像这样\n还有个缺少一些文件产生的问题，我自己重新打包的时候发现并没有碰到，测试了下发现无法复现，只好作罢。只记得那个要在Pyinstaller -F后面加一串来着。\n3.日后谈 # 有些问题我当时使用的时候碰到了，现在没法复现，我自己也无法找到当时找的帖子了，有不少疏忽非常抱歉。(之后要是接着更新也说不定)\n如有建议，欢迎留言，本人必洗耳恭听。\n","date":"2022 年 02 月 09 日","externalUrl":null,"permalink":"/posts/pyinstaller%E7%9A%84%E7%AE%80%E5%8D%95%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8/","section":"Posts","summary":"","title":"Pyinstaller的简单安装使用","type":"posts"},{"content":"","date":"2022 年 02 月 09 日","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"本人是根据这个帖子找到的方法，还有这个帖子提供的代码做出来的。\n代码如下：\n在”页尾信息“可输入这段代码\n网站运行：\u0026lt;span id=\u0026#34;htmer_time\u0026#34; style=\u0026#34;color: red;\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 这个设置出来文字是红色，可以根据需要修改\n需要什么颜色就可以在上方代码中的color后修改成自己想要的颜色（英文名称或者 十六进制颜色表都可以）。\n我设置的是#5e5e5e，仅供参考\n其中”网站运行“可以需改成需要的文字。\n后面的”页尾附加代码“部分则输入下面这段代码；\n\u0026lt;script\u0026gt; function secondToDate(second) { if (!second) { return 0; } var time = new Array(0, 0, 0, 0, 0); if (second \u0026gt;= 365 * 24 * 3600) { time[0] = parseInt(second / (365 * 24 * 3600)); second %= 365 * 24 * 3600; } if (second \u0026gt;= 24 * 3600) { time[1] = parseInt(second / (24 * 3600)); second %= 24 * 3600; } if (second \u0026gt;= 3600) { time[2] = parseInt(second / 3600); second %= 3600; } if (second \u0026gt;= 60) { time[3] = parseInt(second / 60); second %= 60; } if (second \u0026gt; 0) { time[4] = second; } return time; } \u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; language=\u0026#34;javascript\u0026#34;\u0026gt; function setTime() { // 博客创建时间秒数，时间格式中，月比较特殊，是从 0 开始的，所以想要显示 5 月，得写 4 才行，如下 var create_time = Math.round(new Date(Date.UTC(2022, 1, 1, 10, 01, 0))//这里的(new Date(Date.UTC(2022, 1, 1, 10, 01, 0)) .getTime() / 1000); // 当前时间秒数,增加时区的差异 var timestamp = Math.round((new Date().getTime() + 8 * 60 * 60 * 1000) / 1000); currentTime = secondToDate((timestamp - create_time)); currentTimeHtml = currentTime[0] + \u0026#39;年\u0026#39; + currentTime[1] + \u0026#39;天\u0026#39; + currentTime[2] + \u0026#39;时\u0026#39; + currentTime[3] + \u0026#39;分\u0026#39; + currentTime[4] + \u0026#39;秒\u0026#39;; document.getElementById(\u0026#34;htmer_time\u0026#34;).innerHTML = currentTimeHtml; } setInterval(setTime, 1000); \u0026lt;/script\u0026gt; 完成以后保存，可以去首页看看了。\n就是这样的一个效果：\n如果觉得这样在同一行不好看的话，可以加个换行符，这样：\n就是HTML换行符。\n显示效果最后就是这样：\n","date":"2022 年 02 月 06 日","externalUrl":null,"permalink":"/posts/sakurairo%E4%B8%BB%E9%A2%98%E8%AE%BE%E7%BD%AE%E9%A1%B5%E5%B0%BE%E6%98%BE%E7%A4%BA%E7%BD%91%E7%AB%99%E7%B4%AF%E8%AE%A1%E8%BF%90%E8%A1%8C%E6%97%B6%E9%97%B4/","section":"Posts","summary":"","title":"sakurairo主题设置页尾显示网站累计运行时间","type":"posts"},{"content":"这几天再整这个主题的时候 发现了个问题，就是设置的分类，点击会404报错，如下图：\n最初以为是我的Nginx的问题，就查了下，发现没有作用，之后就找到主题的GitHub反馈页找了下才找到解决办法。\n在这个帖子里找到的解决办法\n伪静态的话，我用的是宝塔Linux面板，就直接很容易找到：\n在站点的设置界面就可以找到了\n就是这段代码:\nlocation / { try_files $uri $uri/ /index.php?$args; } rewrite /wp-admin$ $scheme://$host$uri/ permanent; 尽量不要用网上找到的代码，\n否则进入后台的时候会有如下错误：\n让我折腾到了半夜\n设置好以后就可以舒舒服服的写文章啦╰(￣ω￣ｏ)\n","date":"2022 年 02 月 06 日","externalUrl":null,"permalink":"/posts/sakurairo%E4%B8%BB%E9%A2%98%E7%9A%84%E5%88%86%E7%B1%BB404%E6%8A%A5%E9%94%99%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/","section":"Posts","summary":"","title":"sakurairo主题的分类404报错的解决方法","type":"posts"},{"content":"","date":"2022 年 02 月 06 日","externalUrl":null,"permalink":"/tags/%E7%BD%91%E7%AB%99/","section":"Tags","summary":"","title":"网站","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]