基础知识了解

什么是网页?

网页是一个包含HTML标签的纯文本文件,它可以存放在世界某个角落的某一台计算机中,是万维网中的一页,是超文本标记语言格式。它通常是由图片、文字、链接、声音、视频等元素组成。通过网页浏览器访问。

什么是HTML?

HTML译为超文本标记语言,用来制作网页的一种标签语言,自己有对应的标签,文件后缀为.HTML。

什么是浏览器

国际常用:

IE、谷歌Chrome、Edge、苹果Safar、欧朋OPera. 火狐。

谷歌最大,前端专用,使用的是webkit分支的Blink内核,
非常好用,属于二次开发的,借用别人的好处,出生于2008年。

浏览器中的浏览器内核

浏览器内核的作用:用来读取内容,整理信息。

什么是Web标准

是由W3C(万维网联盟)标准组成,包括结构HTML、表现CSS、行为JS三部分组成。

作用:


在使用不同的浏览器下保证网页内容正常统一展示,不乱不错。

Web标准的构成

标准作用
结构结构用于对网页元素进行整理和分类,主要指的是HTML
表现表现用于设置网页元素的版式、颜色、大小等外观样式,主要指的是CSS
行为行为是指网页模型的定义及交互的编写,主要指的是Javascript

第一章语法规则

1. html语法规则

所有双标签存在闭合关系,及存在双标签的开头那么它必须要有结尾

闭合标签:

由开始标签和结束标签共同组成,成对出现常见的闭合标签有<div></div><p></p>、<span></span>等。标签之间的关系可以存在并列及嵌套关系。

单标签:

指没有内容的标签,在开始标签中自带闭合标识符。常见的单标签有<img/>、<hr/>、<link/>、<meta/>、<input/>、<br/>等。


所有的标签都要包裹在一对尖扣号中 “<> ”.

大部分都要标签都要成对出现,成对出现的标签称为 “双标签”: <开始标签> </结束标签>

少部分没有结束标签的称为“单标签”:< 单标签 / >

标签关系分为:包含关系(父子关系) 和 并列关系(兄弟关系)
二、 HTML文档结构基本组成与标签关系

    <HTML>        // 最大的标签称为“根标签”  与 "<head><tile><body>" 标签为包含关系,是父亲其他是儿子关系
      <head>      // 文档头部  与<HTML>是包含关系,与<body>标签是并列关系,
         <tile>文档标题是我</tile>         // 标题标签 与<head>标签是包含关系
      </head>
      <body>     // 文档的主体   与<head>标签是并列关系,与<HTML>是包含关系
        我是整个文档的主体
      </body>
    </HTML>

三、关于vscode自动生成 HTML文档结构 的说明

   
<!DOCTYPE html>  <!--声明为HTML5文档 文档类型声明标签,告诉浏览器用那种HTML版本进行编写显示网页的-->
                 <!--不属于HTML标签,要写在HTML文档最最最顶端-->
<html lang="en">   <!--当前文档显示语言,告诉浏览器或搜索引擎以哪种语言展示网页-->
    <head>    <!--定义了HTML文档的开头部分.它们之间的内容不会在浏览器的文档窗口显示.包含了文档的元(meta)数据-->
        <meta charset="UTF-8">  <!--使用charset字符集和UTF-8万国码声明编码,全国通用不会产生乱码--> 
                          <!--设置GBK为默认编码,则你需要设置为 <meta charset="gbk">-->
        <meta http-equiv="X-UA-Compatible" content="IE=edge">  <!--告诉IE以最高级模式渲染文档-->
        <meta name="viewport" content="width=device-width, initial-scale=1.0">   <!--移动端适配-->
        <title>css样式优先级</title>  <!--定义了网页标题,在浏览器标题栏显示-->
    </head> 
    <body>     <!--文档的主体,与<head>标签是并列关系,与<HTML>是包含关系-->
        我是整个文档的主体
    </body>
</html>

四、HTML常用标签
标签的语义就是标签的含义,用来干什么的,就是让页面结构更清晰的。

1.head标签内的常用标签

<title></title> 定义网页标题

<style></style> 定义内部样式表

<script></script> 定义JS代码或引入外部JS文件

<link/> 引入外部样式表文件

<meta/> 定义网页原信息

<base/> 页面中所有链接默认的链接目标地址
  • Meta标签介绍:
  • 元素可提供有关页面的元信息(mata-information),针对搜索引擎和更新频度的描述和关键词。
  • 标签位于文档的头部,不包含任何内容。
  • 提供的信息是用户不可见的。
  • meta标签的组成:meta标签共有两个属性,它们分别是http-equiv属性和name 属性,不同的属性又有不同的参数值,这些不同的参数值就实现了不同的网页功能。
    1.http-equiv属性:相当于http的文件头作用,它可以向浏览器传回一些有用的信息,以帮助正确地显示网页内容,与之对应的属性值为content,content中的内容其实就是各个参数的变量值。
<!--2秒后跳转到对应的网址,注意引号-->
<meta http-equiv="refresh" content="2;URL=https://www.oldboyedu.com">
<!--指定文档的编码类型-->
<meta http-equiv="content-Type" charset=UTF8">
<!--告诉IE以最高级模式渲染文档-->
<meta http-equiv="x-ua-compatible" content="IE=edge">

2.name属性:主要用于描述wangye,与之对应的属性值为content,content中的内容主要是便于搜索引擎机器人查找信息和分类信息用的

<meta name="keywords" content="meta总结,html meta,meta属性,meta跳转">
<meta name="description" content="老男孩教育Python学院">

base定义了所有链接的URL,使用 定义页面中所有链接默认的链接目标地址。

<base href="https://www.runoob.com/images/" target="_blank">

第二章 标签讲解

1.标题标签

当作标题使用的,依据重要递减。 特点: 文字加粗、独占一行、从重到轻。

<h1>一级标题我最大,只能有我自己,利于搜索引擎优化 </h1>

<h2> 二级标题 </h2>

<h3> 三级标题 </h3>

<h4> 四级标题 </h4>

<h5> 五级标题 </h5>

<h6> 六级标题 </h6>

2.段落标签

把文本分成若干份。 特点: 会自动换行、段与段之间有较大的空隙、每行为独占一行或多行在一起称为一段。

<p>这里写内容</p>

3.图像标签

展示图片。 特点:是单标签、其中src属性是必填属性,共6个属性,属性以 [ 键值对 ]形式:属性=“属性值” 即key=“value”

<img src="imgs/01.jpg" alt="图片未显示用我来提示" title="我是图片的标题" width="100" height="100" border="20"/>

所有属性都要写在img后面,6个属性不分先后顺序,多个属性之间用空格隔开,属性是以键值对形式:key=“value” 即:属性=“属性值 ”
属性具体细化为:

  • src=“图片名称和图片路径”
  • alt=“替换文本” 图像无法显示的时候使用文字代替
  • title="提示文本“ 鼠标悬停在图片上显示的文字
  • width=“图片宽度” 指定图片的宽度,不指定高度的时候,宽度会等比缩放,指定宽度不恰当时图片会变形,故只设置一个值即可
  • height=“图片高度” 指定图片的高度,不指定款度的时候,高度会等比缩放,指定高度不恰当时图片会变形,故只设置一个值即可
  • border=“图片边框粗细” 不需要加单位p x , 该属性文字会显示白色,不常用,在CSS中设置更好。

4.换行标签

强制换行 特点:单标签、 会另起一行、行与行之间的间距很紧密。

<br />

5.文本格式化标签

突出文字重要性。特点:都是双标签

<strong></strong> 加粗

<em></em> 倾斜

<del></del> 删除

<ins></ins> 下划线

6.水平线标签

一条横线贯通整个页面。 特点:单标签

  <hr />

7.音频标签

播放音乐。 特点:谷歌浏览器禁用自动播放音乐,需要调出播放控件方可。

<audio src="音频路径" controls loop></audio>**

属性具体细化为:

src=“音频路径”
autoplay 自动播放,但是谷歌浏览器禁用自动播放功能需要调用controls属性,完成自动播放。
controls 调用播放控件。
loop 循环播放
当属性🟰属性值的时候,就写属性就行。例:controls=“controls”

8.视频标签

播放视频。 特点:谷歌浏览器禁用自动播放音乐,需要调出播放控件方可


<video src="视频路径" controls loop muted ></video>

属性具体细化为:

src=“视频路径”
autoplay 自动播放,但是谷歌浏览器禁用自动播放功能需要调用controls属性,完成自动播放。
controls 调用播放控件。
loop 循环播放
muted 在谷歌浏览器上自动静音播放
当属性🟰属性值的时候,就写属性就行。例:controls=“controls”

9.div标签

无语义,指的就是大盒子 布局网页的。 特点:一行只能用一个盒子,独占一行。

<div> 这里可以包含任何标签 </div>

10.span标签

无语义,指的就是小盒子 布局网页的。 特点:一行内可以有多个,横着水平显示。

<span> 这里可以包含任何标签 </span>

11.超链接标签

跳转页面的 。 特点:双标签,有2个属性 ,href为必填属性,target属性默认值为self。

<a href="跳转的目标" target="打开窗口的方式"> 跳转需要的文字内容 </a>
  • href属性代表=“跳转的目标”
  • target属性代表=“打开窗口的方式”:打开窗口的方式有2个值可选:1是self代表当前窗口打开,2是_blank代表新窗口打开。

href属性的属性值将链接分类为6种:

1.外部链接: 在当前去别的地址,必须以http://开始。 ️herf="http://www.baidu.com "

2.内部链接: 网站内部页面之间的相互链接。 直接写内部链接的文件名字即可 , herf="home.html"

3.空链接: 不确定跳转目标时,用 # 代替。 herf=“#”

4.下载链接:链接的是一个文件或者压缩包,点击后会自动下载 herf=“img.zip”

5.网页元素链接:可以包裹 文字、图片、视频、音频等标签当作链接进行跳转。

6.锚点链接:快速定位到页面中的某个位置, 举例:

2个步骤:

01:在链接文本的href属性中设置属性值为#文件名, <a href="#go">去指定的位置</a>

02:找到要跳到位置的标签,在标签中添加一个 ID属性 = 刚才的名字, <h2 id="go">就跳到我这个位置来></h2>

12.HTML注释

vscode快捷键:ctrl+/ 行注释。 < !-- --> ,只给程序员自己看,代码不会显示到网页中 .。

13.表格标签

表格用于显示和展示数据的

1.表格的基本语法

<table>  //定义表格的标签
    <tr>   //表格中的每一行,必须包含在<table>标签内。
      <td> 单元格 </td>   //表格中每一行里的单元格,必须包含在<tr>标签内
  </tr>
</table>

2.表格的标签

<table>  //定义表格标签
  <caption>    // 表格的大标题 
     <h3>表格的大标题</h3>
  </caption>
  <thead> //表格头部标签
    <tr>   //行标签
           <th> 1 </th>  //表头单元格 
      <th> 2 </th> 
    </tr>
  </thead>
  <tbody>  //表格主体
    <tr>   
          <td> 单元格 </td> 
      <td> 单元格 </td>
  </tr>
  </tbody>
</table>

3.表格属性

  • align —— 表格对齐方式 left center right
  • border ——表格边框 1 或 " "
  • cellpadding ——单元边与内容之间的空白 默认1像素。
  • cellspacing—— 单元格与单元格之间的空白 默认2像素。
  • width—— 表格宽度 单位为px 或 %

4.合并单元格

三要素:

1.搞清楚跨行(垂直方向) 跨列(水平方向)

2.找到合并的单元格

3.在第一个单元格上跨行 rowspan=“合并的数量” 跨列 colspan=“合并的数量” 不要的单元格 删除掉(注释掉)

14.列表 标签

列表是用来布局的。

特点:整齐、整洁、有序。

分三类: 无序列表 / 有序列表 / 自定义列表

无序列表 <ul> <li></li> </ul>

特点:无顺序、并列的、ul只能嵌套li,但li可以潜逃任何标签,有自己的样式,实际中使用c s s设置样式。

有序列表 <ol> <li></li> </ol>

特点:有顺序、ol只能嵌套li,但li可以潜逃任何标签,有自己的样式,实际中使用c s s设置样式。

自定义列表 <dl> <dt></dt> <dd></dd> </dl>

特点:dl只能包含 dt和dd , dt和dd个数没有限制,一个d t对应多个d d可以放任何标签。

15.表单标签

HTML 表单用于收集用户的输入信息。

将表单域区域的内的内容发送到 Web 服务器。

表单域 表单元素(控件 ) 提示信息 三部分组成。

表单域

就是包含表单元素的区域。

<form> </form>表单域
    使用<form>标签包裹内的表单元素会提交给服务器。
    name是用来给表单元素起名称的,区分不同的表单元素,每个表单元素都应有自己对应的name属性。
    value 的值 要传到服务器上去的 
    action 收集的信息提交给那个文件处理

  <form action="1.php">
    姓名: <input type="text" name="username">
    <br><br>
    密码: <input type="password" name="pwd">
    <br><br>
    性别: <input type="radio" name="sex" value="man"> 男 
          <input type="radio" name="sex" value="women"> 女
    <br><br>
    <input type="submit" value="提交按钮">
  </form>

表单元素

元素分三类: input输入select下拉菜单textarea文本域

input输入 (input是个单标签。)

<input type=" ” name=" " > 收集用户信息的。
  • 有type|name|valve|checked|maxlength|label属性
  • type属性有10个属性值
  • text 用户输入文本框的,默认20个字符。
  • password 用户输入密码框,默认字符被掩码。
  • placeholder 占位符,提示用户输入内容的文本。
  • radio 单选按钮。
  • checkbok 多选按钮,属性值:checked 默认选中。
  • submit 提交按钮,把表单域form中表单元素里面的值提交给后台服务器。同时可以通过value属性设置 提交按钮的name。
  • reset 重置按钮,点击后恢复表单默认值。
  • button 普通按钮,配合j s的点击事件使用 。
  • file 文件上传 属性值:multiple多文件选择 。
  • hidden 隐藏输入字段的。
  • image 图像格式的提交按钮。
  • name属性 表示input表单元素的名字,使用多选或者单选元素时所以的name的属性值要统一,才可以实现多选一,属性值用户自定义,value值可以不相同(只在单选和多选值name值要一致)。
  • value属性表示input表单元素的默认值, 属性值用户自定义(只在文本框中有值),要传到服务器上的。
  • checked属性 表示input表单元素首次加载默认选中,属性值只有一个为checked
  • maxlength属性 表示输入字段最大长度,属性值必须为正整数,用户最多输入几个字符。

label标注标签,不属于表单标签,但是常和input标签一起使用,会扩大点击范围浏览器会自动对焦,增强用户体验。

使用方法①:

1.将文字使用

select标签在网页中提供多个选择项的下拉菜单表单控件,是个双标签。

<form>
  <select>  
    <option>上海</option>  
    <option selected=“selected” >北京</option>  
    <option>深圳</option>  
    </select>
</form>

使用规则:

select标签中至少包含一个标签
在option标签中添加 属性 selected=“selected” 表示默认选中
要包含在form表单域中
3.textarea文本域在网页中提供可输入多行文本的表单控件,常用做留言板等

​ 有两个属性 cols=“一行字内的个数”与rows=“行数”

​ 也要包含在form表单域中

代码演示:

<form>
  <textarea cols="100" rows="10">我是默认的提示文字信息</textarea>
</form>

使用规则:

右下角可以拖拽改变大小
实际开发时针对于样式效果推荐用CSS设置

并发编程

进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。

协程(goroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。

业界将如何实现并发编程总结归纳为各式各样的并发模型,常见的并发模型有以下几种:

  • 线程&锁模型
  • Actor模型
  • CSP模型
  • Fork&Join模型
    Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式。

goroutime

Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。

Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了,就是这么简单粗暴。


Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine ,从而让该函数或方法在新创建的 goroutine 中执行。

go关键字

// 创建一个新的 goroutine 运行函数f
go f()  
//匿名函数也支持使用go关键字创建 goroutine 去执行。
go func(){
// ...
}()

启动单个goroutine(代码中 hello 函数和其后面的打印语句是串行的。)

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("hello")
}

func main() {
    hello()
    fmt.Println("你好")
}

接下来我们在调用 hello 函数前面加上关键字go,也就是启动一个 goroutine 去执行 hello 这个函数。

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

这一次的执行结果只在终端打印了”你好”,并没有打印 hello。这是为什么呢?

其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine 。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”。

main goroutine 就像是《权利的游戏》中的夜王,其他的 goroutine 都是夜王转化出的异鬼,夜王一死它转化的那些异鬼也就全部GG了。

所以我们要想办法让 main 函数‘“等一等”将在另一个 goroutine 中运行的 hello 函数。其中最简单粗暴的方式就是在 main 函数中“time.Sleep”一秒钟了(这里的1秒钟只是我们为了保证新的 goroutine 能够被正常创建和执行而设置的一个值)。

按如下方式修改我们的示例代码。

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("hello")
}

func main() {
    go hello()
    fmt.Println("你好")
    time.Sleep(time.Second)
}

在上面的程序中使用time.Sleep让 main goroutine 等待 hello goroutine执行结束是不优雅的,当然也是不准确的。

Go 语言中通过sync包为我们提供了一些常用的并发原语,我们会在后面的小节单独介绍sync包中的内容。在这一小节,我们会先介绍一下 sync 包中的WaitGroup。当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法。

下面的示例代码中我们在 main goroutine 中使用sync.WaitGroup来等待 hello goroutine 完成后再退出。

package main

import (
    "fmt"
    "sync"
)

// 声明全局等待组变量
var wg sync.WaitGroup

func hello() {
    fmt.Println("hello")
    wg.Done() // 告知当前goroutine完成
}

func main() {
    wg.Add(1) // 登记1个goroutine
    go hello()
    fmt.Println("你好")
    wg.Wait() // 阻塞等待登记的goroutine完成
}

将代码编译后再执行,得到的输出结果和之前一致,但是这一次程序不再会有多余的停顿,hello goroutine 执行完毕后程序直接退出。

启动多个goroutine

在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine 。让我们再来看一个新的代码示例。这里同样使用了sync.WaitGroup来实现 goroutine 的同步。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("hello", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码会发现每次终端上打印数字的顺序都不一致。这是因为10个 goroutine 是并发执行的,而 goroutine 的调度是随机的。

动态栈

操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
goroutine的栈大小限制可以达到1GB

goroutine调度

操作系统内核在调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu周期。

区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。

在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型。gpm.png

其中:

  • G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。

全局队列(Global Queue):存放等待运行的 G。

  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
  • P:表示 goroutine执行所需的资源,最多有 GOMAXPROCS 个。

P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。

Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine 则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。

golang runtime是有个sysmon的协程,他会轮询的检测所有的P上下文队列,只要 G-M 的线程长时间在阻塞状态,那么就重新创建一个线程去从runtime P队列里获取任务。先前的阻塞的线程会被游离出去了,当他完成阻塞操作后会触发相关的callback回调,并加入回线程组里。简单说,如果你没有特意配置runtime.SetMaxThreads,那么在没有可复用的线程的情况下,会一直创建新线程。


GOMAXPROCS

我们知道可以通过runtime.GOMAXPROCS()来了设定P的值

Go 1.5开始, Go的GOMAXPROCS默认值已经设置为 CPU的核数, 这允许我们的Go程序充分使用机器的每一个CPU,最大程度的提高我们程序的并发性能。

但其实对于IO密集型的场景,我们可以把GOMAXPROCS的值超过CPU核数,在笔者维护的某个服务中,将GOMAXPROCS设为CPU核数的2倍,压测结果表明,吞吐能力大概能提升10%

runtime.GOMAXPROCS(1) // 代表只用一个CPU核去跑下面的代码

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信
如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是 Go 语言中一种特有的类型。声明通道类型变量的格式如下:

var 变量名称 chan 元素类型

其中:

  • chan:是关键字
  • 元素类型:是指通道中传递元素的类型

    var ch1 chan int   // 声明一个传递整型的通道
    var ch2 chan bool  // 声明一个传递布尔型的通道
    var ch3 chan []int // 声明一个传递int切片的通道

channel零值

未初始化的通道类型变量其默认零值是nil。

var ch chan int
fmt.Println(ch) // <nil>

初始化channel

make(chan 元素类型, [缓冲大小])
  • channel的缓冲大小是可选的。
    举几个例子:

    ch4 := make(chan int)
    ch5 := make(chan bool, 1)  // 声明一个缓冲区大小为1的通道

channel操作

通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用<-符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)

发送

将一个值发送到通道中。


ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

我们通过调用内置的close函数来关闭通道。
通道关闭后,仍然可以进行读操作,但不可以进行写操作。

close(ch)

注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

对一个关闭的通道再发送值就会导致 panic。
对一个关闭的通道进行接收会一直获取值直到通道为空。
对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
关闭一个已经关闭的通道会导致 panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下如下代码片段。

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        .../main.go:8 +0x54

deadlock表示我们程序中的 goroutine 都被挂起导致程序死锁了。为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

其中一种可行的方法是创建一个 goroutine 去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}

func main() {
    ch := make(chan int)
    go recv(ch) // 创建一个 goroutine 从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

首先无缓冲通道ch上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10。

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。

有缓冲的通道

还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

多返回值模式

当向通道中发送完数据时,我们可以通过close函数来关闭通道。当一个通道被关闭后,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值。通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢?

对一个通道执行接收操作时支持使用如下多返回值模式。

value, ok := <- ch

其中:

  • value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
  • ok:通道ch关闭时返回 false,否则返回 true。

下面代码片段中的f2函数会循环从通道ch中接收所有值,直到通道被关闭后退出。

func f2(ch chan int) {
    for {
        v, ok := <-ch
        if !ok {
            fmt.Println("通道已关闭")
            break
        }
        fmt.Printf("v:%#v ok:%#v\n", v, ok)
    }
}

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    close(ch)
    f2(ch)
}

for range接收值

通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。

通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。

注意:目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。

单向通道

在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数中只能执行发送或只能执行接收操作。想象一下,我们现在有Producer和Consumer两个函数,其中Producer函数会返回一个通道,并且会持续将符合条件的数据发送至该通道,并在发送完成后将该通道关闭。而Consumer函数的任务是从通道中接收值进行计算,这两个函数之间通过Processer函数返回的通道进行通信。完整的示例代码如下。

package main

import (
    "fmt"
)

// Producer 返回一个通道
// 并持续将符合条件的数据发送至返回的通道中
// 数据发送完成后会将返回的通道关闭
func Producer() chan int {
    ch := make(chan int, 2)
    // 创建一个新的goroutine执行发送数据的任务
    go func() {
        for i := 0; i < 10; i++ {
            if i%2 == 1 {
                ch <- i
            }
        }
        close(ch) // 任务完成后关闭通道
    }()

    return ch
}

// Consumer 从通道中接收数据进行计算
func Consumer(ch chan int) int {
    sum := 0
    for v := range ch {
        sum += v
    }
    return sum
}

func main() {
    ch := Producer()

    res := Consumer(ch)
    fmt.Println(res) // 25

}

从上面的示例代码中可以看出正常情况下Consumer函数中只会对通道进行接收操作,但是这不代表不可以在Consumer函数中对通道进行发送操作。作为Producer函数的提供者,我们在返回通道的时候可能只希望调用方拿到返回的通道后只能对其进行接收操作。但是我们没有办法阻止在Consumer函数中对通道进行发送操作。

Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况。

<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收

其中,箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。另外对一个只接收通道执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成。

我们使用单向通道将上面的示例代码进行如下改造

// Producer2 返回一个接收通道
func Producer2() <-chan int {
    ch := make(chan int, 2)
    // 创建一个新的goroutine执行发送数据的任务
    go func() {
        for i := 0; i < 10; i++ {
            if i%2 == 1 {
                ch <- i
            }
        }
        close(ch) // 任务完成后关闭通道
    }()

    return ch
}

// Consumer2 参数为接收通道
func Consumer2(ch <-chan int) int {
    sum := 0
    for v := range ch {
        sum += v
    }
    return sum
}

func main() {
    ch2 := Producer2()
  
    res2 := Consumer2(ch2)
    fmt.Println(res2) // 25
}

这一次,Producer函数返回的是一个只接收通道,这就从代码层面限制了该函数返回的通道只能进行接收操作,保证了数据安全。很多读者看到这个示例可能会觉着这样的限制是多余的,但是试想一下如果Producer函数可以在其他地方被其他人调用,你该如何限制他人不对该通道执行发送操作呢?并且返回限制操作的单向通道也会让代码语义更清晰、更易读。

在函数传参及任何赋值操作中全向通道(正常通道)可以转换为单向通道,但是无法反向转换。

var ch3 = make(chan int, 1)
ch3 <- 10
close(ch3)
Consumer2(ch3) // 函数传参时将ch3转为单向通道

var ch4 = make(chan int, 1)
ch4 <- 10
var ch5 <-chan int // 声明一个只接收通道ch5
ch5 = ch4          // 变量赋值时将ch4转为单向通道
<-ch5

reflect

reflect包
在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。
在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。

package main

import (
    "fmt"
    "reflect"
)

type Cat struct {
}

func reflectType(x interface{}) {
    val := reflect.TypeOf(x)
    fmt.Println(val)
    fmt.Printf("type:%v kind:%v\n", val.Name(), val.Kind())

}

func reflectValue(x interface{}) {
    val := reflect.ValueOf(x)
    key := val.Kind()

    switch key {
    case reflect.Int64:
        fmt.Println("type is Int64, value is  ", int64(val.Int()))
    case reflect.Float32:
        fmt.Println("type is Float32, value is  ", float32(val.Float()))

    }

}

func main() {

    var a int64 = 1
    reflectType(a)
    var b float32 = 0.1
    reflectType(b)

    var c = Cat{}
    reflectType(c)

    reflectValue(a)
    reflectValue(b)
    reflectValue(c)

}

在reflect包中定义的Kind类型如下:

type Kind uint
const (
    Invalid Kind = iota  // 非法类型
    Bool                 // 布尔型
    Int                  // 有符号整型
    Int8                 // 有符号8位整型
    Int16                // 有符号16位整型
    Int32                // 有符号32位整型
    Int64                // 有符号64位整型
    Uint                 // 无符号整型
    Uint8                // 无符号8位整型
    Uint16               // 无符号16位整型
    Uint32               // 无符号32位整型
    Uint64               // 无符号64位整型
    Uintptr              // 指针
    Float32              // 单精度浮点数
    Float64              // 双精度浮点数
    Complex64            // 64位复数类型
    Complex128           // 128位复数类型
    Array                // 数组
    Chan                 // 通道
    Func                 // 函数
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指针
    Slice                // 切片
    String               // 字符串
    Struct               // 结构体
    UnsafePointer        // 底层指针
)

浅淡微服务的弊端

上篇文章就表面提到了微服务的优点,但是,微服务架构不是银弹
点我传送到上篇文章(浅谈微服务的优点)

注释:
银弹:
银弹这个词,是来源于欧洲中世纪的传说。看过电影黑夜传说的人肯定知道。说的是狼人这样的怪物,一般的子弹是打不死它的。必须使用银子做的子弹才能杀死它。
后来“银弹”这个词就被用来形容,那些特别有效果、一用就很灵的方法。和走街串巷卖的“神药”,有类比的含义。神药是指一种什么病都能治的药,这明摆着是不可能的事情,肯定是骗子。

分布式锁
在分布式系统中,常常需要协调他们的动作。
如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

分布式系统
所谓分布式计算机系统,是指由多台分散的计算机,经互连网络的联接而形成的系统,系统的处理和控制功能分布在各个计算机上。分布式计算机系统又简称为分布式系统。

分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性。

原子性,可以理解为一个事务内的所有操作要么都执行,要么都不执行。

一致性,可以理解为数据是满足完整性约束的,肯定不会出现中间的状态,
比如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣的中间状态。

隔离性,指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。

持久性,指的是一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响。

总结 : 一些更新操作要么都成功,要么都失败。


目录:

  • 成本
  • 隐患以及复杂性
  • 技术栈

成本

1 . 学习/时间成本

微服务的基础设施包括:
CI、CD,限流,熔断,管理协作,分布式技术,
网关,服务监控,日志收集,重复代码
配置中心,负载均衡,发布成本
领域划分和明确
容器相关技术栈等等
也就是说对于服务来说,单个服务变得简单,整体服务变得复杂。
这些菜都端上来,如果没有很好的技术储备,估计是要不消化的。
当一个新人加入团队后,以前的单体式应用很方便于新人学习,只要在代码仓库将服务下载下来,本地启程序跑起来就好,模块与模块之间的调用不用管。
而当服务拆分成微服务之后,对于新人来说,学习成本是非常高的,需要有团队成员讲解这个服务的架构、微服务架构,再一个个的下载下来,解决服务与服务之间的调用问题,才能将服务运行起来。

2 . 金钱成本

不说了,本文句句不提钱,确句句都是钱。

3 . 维护成本

在微服务架构中最理想的模式是每个服务都可以单独运行起来,有自己的业务逻辑、数据库、中间件、机器资源,当业务逻辑改变时,对应功能的开发和部署成本很低。
但随之带来的问题是如何管理微服务拆分带来的多个微服务项目,你可能需要最底层的硬件资源都是容器,便于弹性伸缩,再到开发、测试、发布、运维时需要全自动化的系统,这需要有一个一定规模的团队,并且每个人都要有一定的技术储备。

隐患

1 . 设计

微服务架构的第一个问题是如何设计它,其中包括了如何做好服务间的最小粒度切分。
一个团队不可能再一次就设计出完美的微服务架构,一些微服务(如 PDF 生成器,Word文档分析/处理器)是显而易见的用例。
而只要是设计到处理业务逻辑,你的代码一定以及肯定,会在你思考如何将应用分割成正确的微服务集合之前,四处移动,因为程序员听最多的一句话就是“这个业务有变化”。

2 . 开发复杂性

您需要面对所有分布式系统都需要面对的复杂性,你可能需要在部署、测试和监控上做很多的工作,在服务间调用、服务的可靠性上做很多工作,甚至您还需要处理类似于分布式事务 的问题。尽管一些框架已经解决了许多的问题,但在实施微服务架构之前,您的团队必须储备足够的分布式系统相关的知识体系,以面对很多您在单体架构下可能没有面临过甚至没有考虑过的问题。

3 . 部署复杂性

在部署和管理时,由许多不同服务类型组成的系统的操作比较复杂,这将要求开发、测试及运维人员有相应的技术水平。

4 . 分布式复杂性

对于单体架构来讲,我们可以不使用分布式,但是对于微服务架构来说,分布式几乎是必会用的技术,由于分布式本身的复杂性,导致微服务架构也变得复杂起来。

技术栈(浅浅的举两个例子)

1 . 技术栈太过灵活

当每个人都可以自己主导一个服务的开发时,技术栈选型往往就变得多样化,以我的团队举例,语言有JAVA,PHP,Python,GO,Node,框架有Spring Boot,Gin,这还是大家统一用服务端语言的前提下,而HTTP连接则是显现出技术多样化的最好例子,Python有requests, Go则有 net/http, req, resty 等,他们的功能和能解决的问题都差不多,但是却因为不同成员的不同偏好而引入,而当A成员 去维护B成员的项目时,A成员就不得不再学一个重复的对他无提升的东西。

2 . 微服务难关之分布式

分布式本文以及提到很多次了,微服务技术架构必用分布式部署架构分布式架构将单机部署的业务拆分成多个机器部署,可根据业务情况无限的弹性伸缩,实现高性能、高可用、高并发。
但是使用分布式也存在很多问题,比如数据一致性问题。提供业务的服务不可能让不同的用户访问到的数据不是同一版本,这样整体就都乱了,因此使用了分布式模式之后,跨服务的操作需要分布式事务保障操作的原子性、当多人对同一个服务操作时需要分布式锁保证该操作的原子性。这些都是使用分布式架构带来的额外成本,我们享受了它所带来的福利,也必定要为其付出代价。

小结:

许多公司使用微服务并不是真正需要它们,尽管微服务现在很流行,但它们并不适合初学者。
大多数公司最好的做法是构建一个单体架构的项目,然后在绝对必要的时候将单体的部分拆分到微服务中。
把从头开始的微服务架构的机会留给那些财力雄厚的大型科技公司。

最后,感谢各位大佬的赞助!

1676563505501.jpg

浅谈微服务的优点

微服务是个极其复杂的概念,作者就一下表面问题浅谈一二。
篇幅有限,涉猎不深,有兴趣还请自行查阅。

目录

  • 微服务的意义
  • 架构理念
  • 微服务的好处
  • 微服务框架

术语解释

RPC:全称 Remote Procedure Call 中文 远程过程调用, 可以理解为调用远程机器上的方法。
单体架构:所有业务代码都在一起。

微服务的意义

我们在开发一个基础的商城程序时, 可能会包含若干模块, 如:用户模块 商品模块 广告模块 订单模块。
在系统建设初期,为了追求系统快速上线,我们可能会把一整套的模块代码, 都放到一个项目代码中。
这样的弊端:
1 . 在后期流量上来后我们发现某个模块功能失效,导致整个项目瘫痪,
举例:
公司进行业务推广,广告模块流量激增,但在此之前,没有做服务拆分,广告模块的高流量导致数据库带宽无法支撑,最终整个项目进入黑洞状态,用户无法下单,就连商城的界面也打不开。
上面的例子简单说明了传统开发模式的弊端,那么,将微服务理念加入之后呢?
示例:
在莫名的建议下,开发组将商城系统进行了模块服务拆分,这样广告模块就是一个独立的服务,用户参与活动时,直接从客户端调用活动服务,活动服务需要验证其他模块数据的时候,又通过RPC调用进行服务间的数据交互,从而实现压力的分摊,不在让全部服务压力都积压到单台服务器或者数据库上。

架构理念

随着需求的迭代,新功能的诞生,代码库会越来越大,尽管我们竭尽全力,希望将巨大的代码库做到清晰的模块化,但事实上模块与模块之间的界限很难划清,慢慢的,相似的代码随处可见,冗余的代码越来越多,修复bug和新功能越发吃力。
微服务则将这一理念应用在独立的服务上,根据业务的边界确定服务的边界,每个服务专注于服务边界之内的事情,因为可以避免很多由于代码库过大衍生出来的各种问题。
那么一个微服务到底应该多微小?足够小即可,不要过小。那么怎么衡量一个系统是否拆足够小了呢?当你面对这个系统时,不会再有 "过大" 想要拆小它的欲望时,那么它应该就足够小了。使用的服务越小,独立性带来的好处就越多,但管理大量的服务也会更加复杂

微服务的好处

  1. 扩展,如果使用较小的多个服务,则可以只对需要扩展的服务进行扩展,这样可以把那些不需要扩展的服务运行在更廉价的服务器上,节省成本。
  2. 可组合性,在微服务架构下,更细粒度的服务拆分会将这一优点体现得更淋漓尽致,像拼图一样组合功能。
  3. 高度自治,一个微服务就是一个独立的实体,可以独立被部署,也可以作为一个进程存在.
  4. 重构性高, 如果面对传统模式的(单体架构)的系统,里面的代码混乱丑陋,没人敢轻易去重构,但是如果你面对的是小规模及其小粒度的服务呢?重构一个服务甚至是重写,都是一件相对容易的事。
    我相信在(单体架构)删除一百行代码,将是一件致命的事情,但是在规划清晰的微服务架构下,删除一个服务也可以游刃有余。

微服务框架

所谓的微服务框架,是一种错误的说法,微服务是一种架构性上的概念,和框架无关。
我们服务间的互相调用,可以用 HTTP 协议 或者是 原生 TCP 协议 来实现,因此实际上,微服务和框架没有一点关系。
如果真要说是微服务框架的话,无非是封装了一些组件,让你更轻松的实现RPC调用
真正的微服务,最核心的其实是如何做好服务间的最小粒度切分,是架构规划的范畴。

小结

本篇主要将微服务和单体架构之间进行了对比,彰显了微服务的优点,具体使用还是要看具体情况。