> 技术文档 > GoWeb开发_go开发web项目

GoWeb开发_go开发web项目

学习目标:

本篇要达到的目的,能为后续复习提供极大便利。

(第3遍复习)

一、网络通信概述

(为本篇基础核心内容)

1、什么是网络通信?

网络通信是指不同设备(如计算机、手机、服务器等)通过计算机网络进行数据交换和信息传递的过程。其核心目标是实现设备之间的互联互通,让数据能够准确、高效地从发送端传输到接收端。

2、网络通信的核心组成部分

  1. 硬件层面

    • 终端设备:发送或接收数据的设备(如手机、电脑、服务器、物联网传感器)。
    • 传输介质:数据传输的物理 / 无线通道,包括:
      • 有线介质:双绞线、同轴电缆、光纤(速度快、稳定性高)。
      • 无线介质:无线电波、微波、蓝牙、Wi-Fi、5G(灵活性高,适合移动场景)。
    • 网络设备:负责数据转发、路由、信号放大等,如路由器、交换机、调制解调器(Modem)、集线器。
  2. 软件层面

    • 通信协议:规定数据格式、传输规则和交互流程的 “语言”(如 TCP/IP、HTTP、FTP)。
    • 操作系统与应用程序:提供网络接口(如 Socket 编程接口),支持上层应用(如浏览器、邮件客户端)实现通信。

3、网络通信的工作原理

第三次复习:(更清晰的认识到了协议)(嘿嘿,收获满满)TCP(传输控制协议)、UDP(用户数据报协议)都属于 TCP/IP 协议族 中的传输层协议IP 地址是网络世界的 “身份证”,用于跨区域 “找到人”;MAC 地址是网络世界的 “门牌号”,用于在具体 “房间”(局域网)内 “敲开门”。两者缺一不可,共同实现从 “全球定位” 到 “最后一公里交付” 的完整通信流程。
  1. 分层模型(以 TCP/IP 为例)
    为简化复杂问题,网络通信采用分层架构,每层负责特定功能,层间通过接口交互。

    • 应用层:直接为用户程序提供服务(如 HTTP 用于网页浏览,SMTP 用于邮件传输)。
    • 传输层:确保端到端的数据传输,主要协议:
      • TCP(面向连接,可靠传输,如网页加载、文件传输)。
      • UDP(无连接,不可靠但高效,如视频流、实时通信)。
    • 网络层:负责网络间的路由和寻址,核心协议是IP(为设备分配 IP 地址,确定数据传输路径)。
    • 数据链路层:在相邻设备间传输数据,处理物理地址(MAC 地址)和错误检测(如以太网协议)。
    • 物理层:定义物理设备的电气、机械特性(如电压、接口标准)。
  2. 数据传输流程

    • 发送端:数据从应用层逐层封装(添加头部信息),最终通过物理层发送。
    • 接收端:数据从物理层逐层解封装(去除头部信息),最终传递给应用层处理。

4、网络通信的主要类型

  1. 按通信对象分类

    • 点对点(Point-to-Point):两台设备直接通信(如蓝牙设备配对)。
    • 点对多点(Point-to-Multipoint):一台设备向多台设备发送数据(如广播、组播)。
    • 端到端(End-to-End):跨越多个网络设备,最终在两个终端间建立逻辑连接(如通过路由器连接的两台远程电脑)。
  2. 按通信方式分类

    • 同步 vs 异步
      • 同步:发送方等待接收方响应(如 TCP 请求 - 响应模式)。
      • 异步:发送方无需等待,直接继续执行(如 UDP 发送数据后不等待确认)。
    • 面向连接 vs 无连接
      • 面向连接:先建立连接(如 TCP 的三次握手),再传输数据(可靠但开销大)。
      • 无连接:直接发送数据(如 UDP,适合实时性要求高的场景)。

5、关键网络协议

  1. 基础协议

    • TCP/IP:互联网的核心协议簇,定义了网络通信的完整架构(包含 IP、TCP、UDP 等)。
    • IP(Internet Protocol):负责设备寻址和路由(IPv4/IPv6)。
    • TCP(Transmission Control Protocol):提供可靠的字节流传输,确保数据无丢失、无乱序。
    • UDP(User Datagram Protocol):提供轻量、快速的数据包传输(不保证可靠性)。
  2. 应用层协议

    • HTTP/HTTPS:用于网页浏览(HTTPS 加密传输)。
    • FTP/SFTP:文件传输协议(SFTP 加密)。
    • SMTP/POP3/IMAP:邮件传输与接收协议。
    • WebSocket:支持浏览器与服务器间的双向实时通信(如在线聊天、实时数据更新)。

二、Socket

基础概念

第一次复习:如果要我解释socket,他就像一门面,封装着各种函数。是一个接口API,正着说可能会让人误解。反着说,socket既不是某种协议,也不是id+端口号的集合,而是操控协议与这个地址结合的工具。他封装着一组函数,通过接口的性质,进行通信。超级方便的哦。(想用即拿)第二次复习:Socket 通信基于客户端 - 服务器(Client - Server)模型第三次复习:小傻🥚,我就知道,你对这里有疑惑,你迟早会去查出来的:API是啥?API(程序编程接口)是什么?有什么用?API是软件用来和外部程序 进行数据交换 的一个渠道,就像一个神明允许凡人借用他的力量。通过API,外部程序可以访问软件的功能,而不需要了解其实现的细节。API的使用非常简单,就像在电脑中访问一个文件,只需要知道他的地址(url)然后提交一些数据作为输入,文件运行完毕后,就会给使用者返回一个结果。API的使用大大降低了程序开发的难度。restful风格 就是 API的一个典型案例,因为是依靠http协议进行传输,所以又叫做web api。(这也是为啥,能实现跨平台配合...为啥能实现不同语言之间的配合...)与函数对比:(浓缩一下)函数 → 库模块 → 服务 → API解释第一次复习给出的答案:Socket 是一种网络编程 API,通过指定 IP 地址、端口号和协议类型(如 TCP 或 UDP),调用其提供的函数实现网络通信。

小demo

根据本图,写相应的代码:

server

package mainimport ( \"fmt\" \"net\" \"strings\")type User struct { Username string Othername string Msg string ServerMsg string}var ( user = new(User) userMap = make(map[string]net.Conn))func main() { // 地址 addr, _ := net.ResolveTCPAddr(\"tcp4\", \"localhost:8889\") lis, _ := net.ListenTCP(\"tcp4\", addr) // 循环接收连接 for { conn, _ := lis.Accept()  go func() { for { b := make([]byte, 1024) count, _ := conn.Read(b) array := strings.Split(string(b[:count]), \"-\") user.Username = array[0] user.Othername = array[1] user.Msg = array[2] user.ServerMsg = array[3] // 加入对方 userMap[user.Username] = conn if v, ok := userMap[user.Othername]; ok && v != nil { // 存在 且 不为空 n, err := v.Write([]byte(fmt.Sprintf(\"%s-%s-%s-%s\", user.Username, user.Othername, user.Msg, user.ServerMsg))) // 关闭连接 if n <= 0 || err != nil {  fmt.Println(\"无效格式\")  delete(userMap, user.Othername)  conn.Close()  return } else {  fmt.Println(\"发送成功\") } } else { user.ServerMsg = \"对方不在线\" conn.Write([]byte(fmt.Sprintf(\"%s-%s-%s-%s\", user.Username, user.Othername, user.Msg, user.ServerMsg))) } void  } }()  }}

client

package mainimport ( \"fmt\" \"net\" \"os\" \"strings\" \"sync\")type User struct { Username string Othername string Msg string ServerMsg string}var ( user = new(User) wg sync.WaitGroup)func main() { wg.Add(1) fmt.Println(\"请输入你的账号\") fmt.Scanln(&user.Username) fmt.Println(\"请输入你要给谁发送消息\") fmt.Scanln(&user.Othername) addr, _ := net.ResolveTCPAddr(\"tcp4\", \"localhost:8889\") conn, _ := net.DialTCP(\"tcp4\", nil, addr) // 发送 go func() { for { fmt.Println(\"请输入,你要发给谁?仅仅只提示一次\") fmt.Scanln(&user.Msg) if user.Msg == \"exit\" { conn.Close() wg.Done() os.Exit(0) } n, _ := conn.Write([]byte(fmt.Sprintf(\"%s-%s-%s-%s\", user.Username, user.Othername, user.Msg, user.ServerMsg))) fmt.Println(n, \"发送成功\") } }() // 接收 go func() { for { rb := make([]byte, 1024) c, _ := conn.Read(rb) user2 := new(User) array := strings.Split(string(rb[:c]), \"-\") if len(array) < 3 { fmt.Println(\"无效格式:\", array) break } user2.Username = array[0] user2.Othername = array[1] user2.Msg = array[2] user2.ServerMsg = array[3] if user2.ServerMsg != \"\" { fmt.Println(\"\\t\\t\\t服务器消息:\", user2.ServerMsg) } else { fmt.Println(user2.Username, \":\", user2.Msg) } } }() wg.Wait()}

三、Mysql

对数据库的操作:

开始之前的基操create database goWeb;use goWeb;create table people( id int primary key auto_increment, name varchar(20), address varchar(100));desc people;select * from people;

增:

package mainimport ( \"database/sql\" \"fmt\" _ \"github.com/go-sql-driver/mysql\")/* 数据库的连接是一个非常有趣的玩意 [user[:password]@][net[(addr)]]/dbname[?param1=value1&param2=value2...] 还有一个奇怪的点,就是必须要导入_ \"github.com/go-sql-driver/mysql\" 因为,他中的init的函数,是sql与go之间的桥梁,起到注册作用register 但是,我没有理解透,感觉好难受*/func main() { // 1、打开链接 db, err := sql.Open(\"mysql\", \"root:1234@tcp(localhost:3306)/goweb\") db.Ping() defer func() { if db != nil { db.Close() } }() if err != nil { fmt.Println(\"数据库连接错误\", err) } // 2、预处理SQL // ?表示占位符 stmt, err := db.Prepare(\"insert into people values(default,?,?)\") if err != nil { fmt.Println(\"预处理失败:\", err) } defer func() { if stmt != nil { stmt.Close() } }() r, _ := stmt.Exec(\"张三\", \"上海\") // 3、获取结果 count, _ := r.RowsAffected() fmt.Println(\"修改了:\", count) // 获取最后修改的主键 fmt.Println(r.LastInsertId())}

删:

package mainimport ( \"database/sql\" \"fmt\" _ \"github.com/go-sql-driver/mysql\")func main() { // 连接 db, err := sql.Open(\"mysql\", \"root:1234@tcp(localhost:3306)/goWeb\") if err != nil { fmt.Println(\"连接失败\", err) } defer db.Close() // 预处理 stmt, err := db.Prepare(\"delete from people where id = 2\") if err != nil { fmt.Println(\"预处理失败:\", err) } defer stmt.Close() r, _ := stmt.Exec() // 预处理 count, _ := r.RowsAffected() if count > 0 { fmt.Println(\"删除成功\") } else { fmt.Println(\"负责删除失败\") }}

改:

package mainimport ( \"database/sql\" \"fmt\" _ \"github.com/go-sql-driver/mysql\")/*这里有一个很有趣的事情,如果修改没变化,则修改失败*/func main() { // 连接 db, err := sql.Open(\"mysql\", \"root:1234@tcp(localhost:3306)/goWeb\") db.Ping() if err != nil { fmt.Println(\"失败啦\", err) } defer db.Close() // 预处理 stmt, err := db.Prepare(\"update people set name = ?,address = ? where id=3 ;\") if err != nil { fmt.Println(\"预处理失败:\", err) } defer stmt.Close() r, _ := stmt.Exec(\"朝阳\", \"新乡\") // 查看修改情况 count, _ := r.RowsAffected() if count > 0 { fmt.Println(\"修改成功\") } else { fmt.Println(\"修改失败\") }}

查:

package mainimport ( \"database/sql\" \"fmt\" _ \"github.com/go-sql-driver/mysql\")/*写完之后,没啥感受,只是在想,这玩意咋都长一个样,背背方法就过去了,可是好像了解了解底层呐*/func main() { // 连接 db, err := sql.Open(\"mysql\", \"root:1234@tcp(localhost:3306)/goWeb\") if err != nil { fmt.Println(\"连接失败\", err) } defer db.Close() // 预处理 stmt, err := db.Prepare(\"select * from people\") if err != nil { fmt.Println(\"预处理失败:\", err) } defer func() { if stmt != nil { stmt.Close() } }() // 获取 rows, err := stmt.Query() if err != nil { fmt.Println(\"获取值出错\", err) } for rows.Next() { var id int var name, address string rows.Scan(&id, &name, &address) fmt.Println(id, \" \", name, \" \", address) } defer rows.Close() // 关闭结果集}

四、goWeb

控制器

第一次复习:

当你再次回来看时,希望这个能加深你的理解:以下三个函数,的区别

Handler 接口 定义 HTTP 处理逻辑的规范(要求实现 ServeHTTP 方法) 所有 HTTP 处理程序必须直接或间接实现此接口

Handle 函数 将一个 Handler 实例绑定到 URL 路径模式(pattern)

参数 handler 必须是 Handler 接口的实现

HandleFunc 函数 便捷方式:将一个函数包装为 Handler 接口的实现,并绑定到 URL 路径模式

本质是 Handle(pattern, HandlerFunc(handler)),简化了手动实现接口的步骤

拓展,实现了handler接口的对象实例,都能被Handle调用。

第三次复习:

  • Handle 的核心作用是将 URL 映射到具体的处理逻辑,负责请求的接收和响应的生成。
  • 单控制器适合简单场景,直接绑定单个 URL;多控制器通过分组管理多个 URL,适合复杂业务的模块化开发。

单控制器

package mainimport ( \"net/http\")/* 何其抽象,这只是一个但控制器 其实就是用结构体,实现一个端口*/type MyHander struct {}func (m *MyHander) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(\"返回的数据\"))}// 单控制器func main() { m := MyHander{} server := http.Server{ Addr: \"localhost:8081\", Handler: &m, // 一旦绑定在这里,无论访问什么路径,结果都是这个 } server.ListenAndServe() //if err := server.ListenAndServe(); err != nil { // fmt.Printf(\"服务器启动失败: %v\\n\", err) // 打印具体错误(如端口冲突) //} }

多控制器

package mainimport \"net/http\"/* 好抽象的呢,既然是重写函数,却要重写的一模一样,抽象啦 简直气死我了 捋一捋思路,发现就是 1、先建立服务器 2、注册路由 3、监听函数*//*func first(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, \"first\")}func second(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, \"second\")}func main() { // 多控制函数 server := http.Server{ Addr: \"localhost:8081\", } // 注册路由 http.HandleFunc(\"/first\", first) http.HandleFunc(\"/second\", second) server.ListenAndServe()}*/// 第二套就是绑定结构体/* 其实多控制器,用结构体,我觉得有点累赘和臃肿 首先重写多个结构体,实现接口,然后将每个结构体,依次绑定到服务器上。 与其用Handle绑定,不如直接用HandleFunc直接绑定 但控制器,就是绑定一个url,多控制器就是绑定多个url。*/type Handle struct {}func (m *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(\"一号\"))}type Handler struct{}func (m *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(\"二号\"))}func main() { h1 := Handle{} h2 := Handler{} // 重写结构体 server := http.Server{ Addr: \"localhost:8081\", } http.Handle(\"/first\", &h1) http.Handle(\"/second\", &h2) // 监听 server.ListenAndServe()}

请求头与请求参数

请求头

package mainimport ( \"fmt\" \"net/http\")func param(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, \"第一个\") // 请求头 //fmt.Fprintln(w, r.Header) // 为了便于读写代码 var acc []string = r.Header[\"Accept\"] for _, n := range acc { fmt.Fprintln(w, \"Accepth内容\", n) }}func main() { // 建立服务器 server := http.Server{ Addr: \"localhost:8081\", } // 绑定url http.HandleFunc(\"/param\", param) // 开启监听模式 server.ListenAndServe()}

请求参数

(可在url 或 请求体中)

package mainimport ( \"fmt\" \"net/http\")/*这里有个有意思的事情,是要用ParseForm去解析表单,才能用r.Form查到因为,需要ParseForm更新并放置于了Form中*/func param(w http.ResponseWriter, r *http.Request) { // 我的名字叫做解析 // 对请求头解析 h := r.Header // header是一个map类型,选中key值后,返回的是一个string类型的切片 fmt.Fprintln(w, h[\"Accept-Encoding\"][0]) // 必须先解析成form r.ParseForm() fmt.Fprintln(w, r.Form)}func main() { // 建立服务器 server := http.Server{ Addr: \"localhost:8081\", } http.HandleFunc(\"/param\", param) // 开始作为服务端监听 server.ListenAndServe()}

html模板与静态资源

main

package mainimport ( \"fmt\" \"html/template\" \"net/http\")/* 第一遍学习时: 这个解析模板,我有点不理解 不是啊,哥们,这有点抽象 第一遍复习: 其实,我很无奈,因为我的目录与课程目录不同 倒逼我去理解,某些函数的作用 开始时,我最苦恼的是,StripPerfex与FileServer的作用。 原来他的作用,就是解析。FileServer起一个拼接的作用,一旦出现 /static/ 拼接的作用,就开始显现 url中 如Handle的作用*/// 与其绑定的url操作func welcome(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles(\"GoWebDevelopment/basis/htmlTest/view/index.html\") if err != nil { fmt.Println(\"出错了: \", err) } t.Execute(w, nil)}func main() { // 服务器 server := http.Server{ Addr: \"localhost:8081\", } http.HandleFunc(\"/1\", welcome) http.Handle(\"/static/\", http.StripPrefix(\"/static/\", http.FileServer(http.Dir(\"GoWebDevelopment/basis/htmlTest/static\")))) // 开启监听 server.ListenAndServe()}

html

  Title  你好啊,哥们 

js

function myClick(){ alert(\"您点击了按钮\")}

函数/数据->模板

难道看到这里你不好奇吗?模板--数据,难道你不好奇吗,中间是怎么传输数据的?中间的具体过程,以及底层实现第一次复习:我当然知道,不就是reflect嘛,但是我不希望就此止步于此第二次复习:采用的都是链式调用(使用方法的精髓)第三遍复习:我发现一个非常有趣的事,每次访问 URL 时,服务器才按需加载模板文件并返回。  因此模板可随时修改,无需重启服务,修改内容下次访问即生效,实现动态更新。

数据->模板

1、向模板传递参数

2、向模板传递结构体

3、向模板传递map

函数->模板

main

package mainimport ( \"fmt\" \"html/template\" \"net/http\" \"time\")/* 说实话走到这里有一个非常抽象的事情,就是模板时间,你所设置的模板时间必须与这个一模一样 这个是模板时间的整体性:\"2006-01-02 15:04:05\" 其实最抽象的是FuncMap这个绑定的函数,我在这里错了好久。 起因却是因为key-value出了问题。 html文件中,用的函数名,就是key值,我之前写的却是 fm := template.FuncMap{\"mt\": GetTime}--“mt” 但是,html中绑定的确是fm,这完全是混洗了概念的 如果不明白上述说的啥,请让ai回溯*/func GetTime(t time.Time) string { return t.Format(\"2006-01-02\")}func welcome(w http.ResponseWriter, r *http.Request) { curtime := time.Date(2018, 1, 2, 3, 4, 5, 0, time.Local) // time.Format:大致意思,就是你给他一个格式,他按照你给的格式编辑。 // 解析成合适的函数 fm := template.FuncMap{\"fm\": GetTime} // 绑定 t := template.New(\"index.html\").Funcs(fm) t, err := t.ParseFiles(\"GoWebDevelopment/basis/htmlTest1/PassFunction/view/index.html\") if err != nil { fmt.Println(\"调用失败:\", err) } t.Execute(w, curtime) // 这个是暂时的}// 打着这个旗号func main() { // 创建服务器 server := http.Server{ Addr: \"localhost:8082\", } http.HandleFunc(\"/2\", welcome) server.ListenAndServe()}

html

  Title 这是是北京时间:{{.}} 
今天是{{.Year}}年
格式化输出就是{{.Format \"2006-01-02\"}}
{{fm .}}

Action

if使用

二元比较(隶属于if)

if..else..if...else

range

main 

package mainimport ( \"fmt\" \"html/template\" \"net/http\")/*1、测试变量-$2、测试 if and if else3、测试 range*/func welcome(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles(\"GoWebDevelopment/basis/action/view/index.html\") if err != nil { fmt.Println(\"解析出错: \", err) } // []string varInt := []int{1, 2, 3, 4, 5} t.Execute(w, varInt)}func main() { server := http.Server{ Addr: \"localhost:8081\", } http.HandleFunc(\"/1\", welcome) server.ListenAndServe()}

html

  Title{{$n:=100}} {{if gt $n 101}} 你好呀
{{else}} 好遗憾,他没看到呢
{{end}} {{range .}}{{.}}
{{end}}

模板嵌套

若我没猜错,你一定会回来看的。

用我复习3遍的经验告诉你,你可以直接看代码

或许你看着他们特别的复杂,其实除了主main函数

三个html函数,都依靠着各自的后背

head index(被定义为了layout) end 以index为主体,通过define定义,以template为连接。将他们链接在一起。

main 

package mainimport ( \"fmt\" \"html/template\" \"net/http\")func welcome(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles(\"GoWebDevelopment/basis/Nesting/view/index.html\", \"GoWebDevelopment/basis/Nesting/view/head.html\", \"GoWebDevelopment/basis/Nesting/view/end.html\") if err != nil { fmt.Println(\"这里出错啦:\", err) } t.ExecuteTemplate(w, \"layout\", nil)}func main() { // 设置服务器 server := http.Server{Addr: \"localhost:8081\"} // 开启 http.HandleFunc(\"/\", welcome) // 开启监听 server.ListenAndServe()}

head

如果爆红,请不要紧张,编辑器的bug,不怪咱{{define \"head\"}}  Title 我是头部{{end}}

index

{{define \"layout\"}}  Title {{template \"head\"}}
你好
{{template \"end\"}}
{{end}}

end

{{define \"end\"}}  Title 这里是结尾呦{{end}}

文件上传

在这里,html表单中的enctype是主角,

我个人感觉,后端只需要接收传递过来的信息,附带加工即可。

html-form表单

  文件上传  用户名:
上传照片:

main

package mainimport ( \"fmt\" \"html/template\" \"io/ioutil\" \"net/http\" \"strings\")/* 第一遍感悟: 从->r.Request接收数据 FormValue接收值-到-FormFile接收文件, 文件又有File(粗略)与FileHeader(详细)两个方向解析 其实到最后,还有一个奇葩的问题,就是form没数据,硬传,定报错,所以要用err 第二遍感悟: 先从http中获取名字(FormValue),在获取文件接口(FormFile),转为2进制(WriteFile),存入本地*/func welcome(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles(\"GoWebDevelopment/basis/upload/view/index.html\") if err != nil { fmt.Println(\"解析模板出错\", err) } t.Execute(w, nil)}func upload(w http.ResponseWriter, r *http.Request) { fileName := r.FormValue(\"name\") fmt.Fprintln(w, fileName) // 检查文件上传错误(关键修正) file, fileHeader, err := r.FormFile(\"photo\") if err != nil { fmt.Fprintln(w, \"错误:未上传文件或请求不合法\") return // 终止函数,避免后续空指针操作 } defer file.Close() // 及时关闭文件流,释放资源 b, err := ioutil.ReadAll(file) // 建议改为 io.ReadAll(file)(ioutil 已弃用) if err != nil { fmt.Fprintln(w, \"错误:读取文件内容失败\", err) return } // 获取后缀 suffix := fileHeader.Filename[strings.LastIndex(fileHeader.Filename, \".\"):] // 建议改为 os.WriteFile(ioutil 已弃用) err = ioutil.WriteFile(\"D:\\\\workspace_go\\\\practice\\\\GoWebDevelopment\\\\basis\\\\upload\\\\file\"+fileName+suffix, b, 0777) if err != nil { fmt.Fprintln(w, \"错误:保存文件失败\", err) return } t, err := template.ParseFiles(\"GoWebDevelopment/basis/upload/view/success.html\") if err != nil { fmt.Fprintln(w, \"错误:解析模板失败\", err) return } t.Execute(w, nil)}func main() { server := http.Server{ Addr: \"localhost:8081\", } http.HandleFunc(\"/1\", welcome) http.HandleFunc(\"/upload\", upload) server.ListenAndServe()}

文件下载

  • 如果照片看不懂,就看我写的简介。
    MIME类似标签,多用途互联网邮件扩展类型,是一种用于标识文档、文件或字节流的性质和格式的标准

基础概念

MIME

MIME类似标签,多用途互联网邮件扩展类型,是一种用于标识文档、文件或字节流的性质和格式的标准

  • 在 HTTP 通信中,服务器通过响应头中的 MIME 类型,让浏览器判断是直接显示内容(如 text/html 类型的网页、image/jpeg 类型的图片),还是提示用户下载(如 application/pdf 类型的文件)。
  • 邮件程序通过 MIME 类型检测附件格式,选择对应程序打开;文件管理器依据 MIME 类型,用合适的应用打开文件、显示文件类型描述及图标等。

main函数

package mainimport ( \"fmt\" \"html/template\" \"net/http\" \"os\")/*关于请求下载,是一件非常有趣的事情老样子,启动服务器,开启监听,绑定路由,用一个页面将基础内容展示出来其次才是新东西,在html中,设置a标签。href设置成请求下载url,启动download路由获取filename参数。开始申请本地文件,通过os.ReadAll转化为2进制,传递到客户端并通过设置客户端标头,Head()...用set改变各种参数content-type与Disposition是客户端下载下来*/func welcome(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles(\"GoWebDevelopment/basis/download/view/index.html\") t.Execute(w, nil)}func download(w http.ResponseWriter, r *http.Request) { // 人家这个函数,只是接收的一个请求而已 filename := r.FormValue(\"filename\") // 获取值 b, err := os.ReadFile(\"GoWebDevelopment/basis/upload/\" + filename) if err != nil { fmt.Fprintf(w, \"下载失败:\", err) return } h := w.Header() h.Set(\"Content-Type\", \"application/octet-stream\") h.Set(\"Content-Disposition\", \"attachment;filename=\"+filename) w.Write(b) /* 我知道,以后看到这里的时候,你一定会有疑惑,(w.Write()与Fprintln(w,)的区别) 没事,我替你解决: 若需要精确控制输出内容(如二进制数据、JSON、无额外字符的文本),选 w.Write([]byte); 若需要快速拼接并输出文本(如调试信息、多变量组合输出),选 fmt.Fprintln(w, ...)。 */}func main() { server := http.Server{ Addr: \"localhost:8081\", } http.HandleFunc(\"/1\", welcome) http.HandleFunc(\"/download\", download) server.ListenAndServe()}

index函数

  下载链接  点击我下载

ajax请求返回json数据

main

package mainimport ( \"encoding/json\" \"fmt\" \"html/template\" \"net/http\")/*我幸运的孩子呐,如果你下载jquery不幸运落坑,这时我建议你,Ctrl+S试试*/type User struct { Name string Age int}// 显示主页面func welcome(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles(\"GoWebDevelopment/basis/ajax/view/index.html\") t.Execute(w, nil)}// 响应ajax请求func showUser(w http.ResponseWriter, r *http.Request) { us := make([]User, 0) us = append(us, User{\"张三\", 12}) us = append(us, User{\"李四\", 13}) us = append(us, User{\"王五\", 14}) b, _ := json.Marshal(us) // 转化成了二进制 w.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\") fmt.Fprintln(w, string(b)) /* 我知道,很多天后,你肯定会出现疑惑!为什么要用string(b),而不直接用b呢?? 哈哈,我来教你: 这是符合 Content-Type: application/json 的正确输出方式,客户端(如前端 Ajax)可以直接解析这个 JSON 字符串。 其他底层的,是与前端接轨的,我一个后端的,暂时不研究 */}func main() { // 启动服务器 server := http.Server{ Addr: \":8888\", } // 处理静态资源 http.Handle(\"/static/\", http.StripPrefix(\"/static/\", http.FileServer(http.Dir(\"GoWebDevelopment/basis/ajax/static/\")))) http.HandleFunc(\"/1\", welcome) http.HandleFunc(\"/showUser\", showUser) // 同步监听 server.ListenAndServe()}

html

html  用户数据展示   $(function () { $(\"button\").click(function () { $.ajax({  url: \"/showUser\",  type: \"GET\",  data: {}, // 若后端需要参数(如分页),可在此添加 {page: 1}  success: function (data) { var res = \"\"; // 遍历服务器返回的 data 数组(假设 data 是 [{Name: \"张三\", Age: 20}, ...]) for (let i = 0; i < data.length; i++) { // 修正:使用 data[i] 而非 resKey[i] res += \"\"; res += \"\" + data[i].Name + \"\"; // 拼接姓名 res += \"\" + data[i].Age + \"\"; // 拼接年龄 res += \"\"; } // 将拼接好的行插入到 tbody#mybody 中 $(\"#mybody\").html(res);  },  error: function (xhr, status, error) { // 错误提示 alert(\"数据加载失败!错误:\" + error);  } }); }); }); <!-- 修正: 移到  内部 -->
<!-- 表头用 包裹(可选但推荐) --> <!-- 表体用 包裹 -->
姓名 年龄

非常有趣的小玩意

解释:

  • 如果是浏览器可直接渲染的内容(如 text/html、image/png、text/plain 等):
    • 浏览器会直接处理并显示。例如:
      • 当响应体是 HTML 内容(Content-Type: text/html),浏览器会解析并渲染成网页。
      • 当响应体是图片(Content-Type: image/png),浏览器会直接显示图片。
      • 当响应体是纯文本(Content-Type: text/plain),浏览器会显示原始文本。
  • 如果是数据格式(如 application/json、application/xml、text/csv 等):
    • 浏览器不会直接渲染,而是将数据 “交给” 前端 JavaScript(如通过 AJAX/Fetch 请求获取),由脚本解析后再决定如何显示(例如更新页面 DOM、弹出提示等)。

这个是关键:

  • 直接显示的情况:当响应体是浏览器可直接渲染的内容(如 HTML、图片、文本),且通过普通导航请求获取时,会直接显示在界面上。
  • 需前端处理的情况:当响应体是数据格式(如 JSON、XML),或通过 AJAX/Fetch 异步获取时,需前端脚本解析并手动更新页面显示。

正则表达式

这里,我建议,最好的学习方式,是看看菜鸟文档 :: 我的笔记 ::

自己的小小测试,掌握到这里就足够了,当然要结合菜鸟看看基础知识点

::可以自己尝试写一个邮箱,用来测试自己::

package mainimport ( \"fmt\" \"regexp\")/*第一次复习的时候,巩固知识点,收获1、动态编译(Compile)2、静态编译(MustCompile)1、匹配(MatchString)、查找(FindAllString)、分割(Split)、替换(ReplaceAllString)*/func main() { // ^与$ 两者结合起来的用法。\\D的用法,反斜杠``转义的用法 flag, _ := regexp.MatchString(`^\\D+$`, \"abs\") fmt.Println(flag) /* 创建一个regexp对象,然后调用方法 */ r := regexp.MustCompile(`\\d`) flag = r.MatchString(\"fsaf\") fmt.Println(flag) // 返回所有切片,-1返回所有,1返回第一个,2返回前两个,3返回前三个 str := r.FindAllString(\"234\", -1) fmt.Println(str) // 按照规则切割,没有的话,返回空。n==0返回空,n0返回对应个数+剩余个数 str = r.Split(\"d12jkj231dd\", -1) fmt.Println(str[0], str) // 就是起到一个替换的作用 st := r.ReplaceAllString(\"d1w2k3k3\", \"美女\") fmt.Println(st)}

Cookie

基础概念:

Cookie 是一种客户端存储技术,用于解决 HTTP 协议无状态的问题。HTTP 协议本身不会记录用户的任何操作状态,而 Cookie 可以让服务器在客户端存储少量数据(以键值对形式),当客户端再次访问服务器时,会将这些 Cookie 携带在请求中发送给服务器,从而实现会话跟踪、用户身份识别等功能。

在 Go 语言中,

net/http 包提供了对 Cookie 的支持,通过 

http.Cookie 结构体来表示一个 Cookie,其常见字段如下:

  • Name:Cookie 的名称(必填)。
  • Value:Cookie 的值。
  • Path:设置 Cookie 的访问范围,默认为 \"/\",表示当前项目下所有路径都可访问该 Cookie。
  • Domain:可访问该 Cookie 的域名。
  • MaxAge:Cookie 的最大存活时间(单位:秒)。
    • MaxAge > 0:表示 Cookie 会在指定秒数后过期。
    • MaxAge = 0:表示不设置 Max - Age 属性。
    • MaxAge < 0:表示立即删除该 Cookie(等价于设置过期时间为过去)。
  • Expires:指定 Cookie 的过期时间(time.Time 类型),与 MaxAge 功能类似,用于兼容老版本浏览器。
  • Secure:若为 true,表示仅在 HTTPS 连接下才发送该 Cookie。
  • HttpOnly:若为 true,表示该 Cookie 不能通过 JavaScript(脚本语言) 访问,可增强安全性(防止 XSS 攻击窃取 Cookie)。

设置(set)、获取(get) Cookie

package mainimport ( \"fmt\" \"html/template\" \"net/http\" \"net/url\")/*为了写这个,真是命运多舛呐cookie本来即使一个很好写的东西无非就是创建cookie,然后通过响应传回去并入到,请求标头中通过request接受这个信息!!! 但前提有一个重要的原因是,必须编译与解码!你可以把url.QueryFiles的作用是将信息转换成%+16进制*/func welcome(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles(\"GoWebDevelopment/basis/Cookie/setAndGetCookie/view/index.html\") t.Execute(w, nil)}func setCookie(w http.ResponseWriter, r *http.Request) { encodeValue := url.QueryEscape(\"成功了\") c := http.Cookie{Name: \"name\", Value: encodeValue, Path: \"/\"} http.SetCookie(w, &c) t, _ := template.ParseFiles(\"GoWebDevelopment/basis/Cookie/setAndGetCookie/view/index.html\") t.Execute(w, nil)}func getCookie(w http.ResponseWriter, r *http.Request) { res := r.Cookies() for n, v := range res { str, _ := url.QueryUnescape(v.Value) fmt.Println(n, \":\", str) } t, _ := template.ParseFiles(\"GoWebDevelopment/basis/Cookie/setAndGetCookie/view/index.html\") t.Execute(w, res)}func main() { server := http.Server{ Addr: \":8888\", } http.HandleFunc(\"/1\", welcome) http.HandleFunc(\"/setCookie\", setCookie) http.HandleFunc(\"/getCookie\", getCookie) server.ListenAndServe()}

拓展:(HttpOnly、Path、MaxAge、Expires、Domain)

package mainimport ( \"net/http\" \"time\")func serv(w http.ResponseWriter, r *http.Response) { // 验证httpOnly // true不允许获得,false允许js脚本获得 c1 := http.Cookie{Name: \"myname\", Value: \"myvalue\", HttpOnly: true} // /abc/ 代表能访问的路径,必须以/abc/以跟路径 c2 := http.Cookie{Name: \"myname\", Value: \"myvalue\", Path: \"/abc/\"} // 设置存货时间 c3 := http.Cookie{Name: \"myname\", Value: \"myvalue\", MaxAge: 10} t := time.Now() // 获取现在时间 c4 := http.Cookie{Name: \"myname\", Value: \"myvalue\", Expires: t} // 必须以这个指定域名结尾,才可使用 c5 := http.Cookie{Name: \"myname\", Value: \"myvalue\", Domain: \".bjsxt.com\"}}func main() { server := http.Server{ Addr: \":8888\", } http.HandleFunc(\"/1\", serv) server.ListenAndServe()}

核心流程:

角色

现实类比

在 Web 中的作用

客户端

去餐厅吃饭的 “顾客”

主动向服务器(餐厅)发送请求(点单),并接收响应(用餐);存储 Cookie(如 “会员卡号”)

服务器

提供服务的 “餐厅”

接收请求(处理点单),返回响应(提供食物);设置 Cookie(如发放 “会员凭证”)

URL

餐厅的 “地址”

客户端通过 URL 找到服务器(餐厅位置),并指定具体资源(如 “餐厅的汉堡套餐”)

Cookie

餐厅发放的 “会员凭证”

服务器(餐厅)发给客户端(顾客)的凭证,客户端下次访问时携带(证明会员身份)

第三方实现Restful风格

简而言之,第三个库实现了一个路由

他只是一种风格,而不是一种协议。
换句话说,他只是将http协议应用的更规范。它基于 HTTP 协议对资源操作进行规范设计

拓展:

一、交换机与路由器:

1、交换机是什么?

交换机就是把数据包发送到正确的位置

交换机相当于邮递员,根据数据包中的目标mac地址,找到它对应的物理端口。

2、交换机与路由器有什么区别

一台交换机有很多个端口。他们都有自己的编号

计算机的网卡通过网线连接到交换机的端口上。这个端口就是一个确定的物理位置。我们只要知道某个网卡的mac地址在哪个端口上,我们就能正确的把数据包发给它。所以在交换机中,有一张端口与mac地址的影射关系表,我们称之为mac地址表。交换机维护这张映射关系表。想要与某个mac地址通信时,只需要来查询一下,这个mac地址在哪个端口上,然后从对应的端口发送出去就可以了。

每一包数据都会有两个mac地址,一个是发送方的mac地址成为源mac,另一个是接收方的mac地址称为目标mac,交换机受到一包数据后,首先要把这包数据的源mac与接收端口进行绑定,然后交换机要根据目标mac查找,从哪个端口把数据包发送出去,这时候就会出现两种情况。第一种是mac地址表中查询到了关联的端口,则直接从关联端口发出第二种情况是mac地址表中没有查询到关联端口。则向除了接收端口之外的所有端口群发,这种行为称之为泛洪,如果目标mac地址在这个网络中则它一定能受到群发的数据包,如此运行一段时间后,通过交换机的mac地址表,就可以找到网络中的所有网卡设备。由此可见,交换机只会关心数据包中的mac地址,而不会关心ip地址,mac地址在TCP/IP协议中处于第二层数据链路层,所以交换机通常也被称为二层设备。

路由器有两种接口,一种是LAN口,一种是WAN口,LAN口可以有多个用来接家庭网络设备。比如,台式机,手机,笔记本,其中手机和笔记本是通过wifi连接到路由器的设备,也相当于连接到了LAN口上。WAN口只有一个,用来接入运用商网络,以连接到互联网中。如果把路由器的WAN口忽略,只用LAN口,其实路由器就是一台交换机。

3、什么是网关?

子网如何划分?

IP地址子网掩码按位相与

“与”的意思就是1与几就是几,而0与几都是0.

我们常用的子网掩码255.255.2555.0 前3个字节也就是前24位全为1,后8位全为0。所以按位相与的结果。

一定是这个IP地址的前三个字节不变,而最后一个字节是0.

比如192.168.1.10与255.255.255.0=192.168.1.0

我们把IP地址与子网掩码相与之后的结果是相同的两个IP认为是在同一个子网中,也就是说IP为192.168.1.10.子网掩码位255.255.255.0的这张网卡与另一个192.168.1.X的网卡一定是在同一个子网之中。

因为子网掩码都是连续的1和连续的0.所以我们通常用1的数量来表示子网掩码。

比如:255.255.255.0.就是24.

我们用IP/子网掩码来表示一个网络。

比如:192.168.1.0/24表示的网络中拥有255个IP地址。

所以如果想扩大子网中IP地址的数量。我们只需要把子网掩码调小,如果想减少网中IP地址的数量,我们只需要把子网掩码调大就可以了。

子网的意义:

因为TCP/IP协议规定,不同子网之间是不可以直接通信的,如果要通信需要通过网关来进行转发。(实现了对应的功能就算网关

网关上有两张网卡。分别配置了属于两个子网的IP地址。可以在两个网络之间转发数据包。这样我们就拥有了一个连接了两个子网的网络。

比如:子网1中的计算机A发送数据包时,首先计算机A会根据目标IP判断是否跟自己属于同一个子网。如果是同一个子网则直接从网卡发出。如果不是同一个子网,则需要把数据包的目标mac地址改为网关mac,然后发送给网关。网关拿到这一包数据后再通过路由表查询到这一包数据属于子网2,网关修改目标mac地址为计算机B的mac地址。修改原mac为自己的mac.然后从子网2的网卡发出。

(ip判断“身份时用的”,mac转发地址时用的)

4、什么是路由

以上出现了多次根据目标IP判断数据包因该如何发送的行为,我们就称之为路由。

路由器有一个WAN口接入互联网。多个LAN口接入本地网络。它们就分别属于两个不同的子网。

所以从内网访问互联网就是跨网络的行为。这时候就需要路由器来担任网关的角色。它的行为就叫路由。

图1 路由器

每天个家庭至少有1个路由器来连接网络。

在结尾处,我知道,你可能还是会有一点点小小的疑惑:

我来解答:

交换机路由器通常配合使用,共同构建完整的网络架构:

1、家庭网络示例:

  • 路由器:连接宽带调制解调器(Modem),作为家庭网络的 “网关”,负责拨号上网、分配 IP 地址(DHCP)、提供 Wi-Fi 服务。
  • 交换机:若家庭设备较多(如多个电脑、游戏机),可通过交换机扩展 LAN 接口,设备通过交换机连接到路由器,间接访问互联网。

2、企业网络示例

  • 路由器:作为企业网络与互联网的边界设备,负责路由选择、流量控制、安全防护(如防火墙)。
  • 交换机:在企业内部,多台交换机级联或堆叠,构建局域网,实现设备互联;核心交换机连接到路由器,使内部设备能够访问外部网络。

交换机像个桥梁。

路由器像个交通枢纽。

二、三次握手

1、什么是三次握手?

三次握手是建立连接的过程。

  1. 当客户端向服务端发起连接时,会先发一包syn包连接请求数据,进行询问,能否建立连接。
  2. 如果服务端同意连接,则回复一包syn+ack包。
  3. 客户端收到之后回复一包ack包,连接建立。
2、为什么要三次握手而不是两次握手呢?

服务端回复完syn+ack之后就建立连接,这是为了防止因为已失效的请求报文,突然又传到服务器引起错误。

  1. 假设采用两次握手建立连接,客户端向服务端发送了一个syn包,来请求建立连接,因为某些未知原因,并没有到达服务器,在中间某个网络节点产生了滞留,为了建立连接客户端会重发syn包。这次的数据包正常送达,服务端回复syn+ack之后建立了连接。
  2. 但是第一包数据阻塞的网点节点,突然恢复,第一包syn包又送达到服务端,这时服务端会误认为是客户端又发起了一个新的连接,从而在两次握手之后,进入等待数据状态,服务端认为是两个连接,而客户端认为是一个连接,造成了状态不一致。
  3. 如果在三次握手的情况下,服务端收不到最后的ack包,自然不会认为连接建立成功,所有三次握手本质上来说,就是为了保证在不可靠的网络链路中,建立起可靠的连接。如syn包阻塞重发会导致服务器创建多重连接,而客户端只接受唯一连接。从而造成状态不一致的情况。
3、什么是四次挥手?

第一次复习:怎么总感觉,四次握手,像一对热恋的人一样呀?分开了,还恋恋不舍😄

处于连接状态的客户端和服务端,都可以发起关闭连接请求,此时需要四次挥手来进行连接关闭。

  1. 假设客户端主动发起连接关闭请求,他需要向服务端发起一包fin包,表示要关闭连接,自己进入终止等待1状态,这是第一次挥手。
  2. 服务端收到fin包,发送一包ack包,表示自己进入了关闭等待状态,客户端进入终止等待2状态,这是第二次挥手
  3. 服务端此时还可以发送未发送的数据,而客户端还可以接收数据,待服务端发送完数据之后,发送一包fin包,进入最后确认状态。这是第三次挥手。
  4. 客户端收到之后回复ack包,进入超时等待状态,经过超时时间后关闭连接,而服务端收到ack包后,立即关闭连接。这是第四次挥手。

为什么客户端需要等待超时时间?

这是为了保证服务端已收到ack包。

  1. 因为假设客户端发送完最后一包ack包后就释放了连接,一旦ack包在网络中丢失,服务端将一直停留在最后确认状态。
  2. 如果客户端发送最后一包ack包后,等待一段时间,这时服务端因为没有收到ack包,会重发fin包,客户端会响应这个fin包,重发ack包并刷新超时时间。
  3. 保证在不可靠的网络链路中,进行可靠的连接断开确认。

为什么要四次挥手?

  1. 由于 TCP 的半关闭(half-close)特性,任何一方都可以在数据传送结束后,发出连接关闭的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接关闭通知,对方确认后就完全关闭了TCP连接。
  2. 通俗的来说,两次挥手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次挥手。
4、丢包问题

5、三次握手详解:

注意:我们上面写的ack和ACK,不是同一个概念:

  • 小写的ack代表的是头部的确认号Acknowledge number, 缩写ack,是对上一个包的序号进行确认的号,ack=seq+1。
  • 大写的ACK,则是我们上面说的TCP首部的标志位,用于标志的TCP包是否对上一个包进行了确认操作,如果确认了,则把ACK标志位设置成1。

借鉴博客:

1、我自己的笔记 

2、菜鸟文档

3、交换机 & 路由器 & 网关 & 子网

4、TCP详解


 ::有道云笔记点击入口::

如果有帮助、记得点赞+收藏呐(〃 ̄︶ ̄)人( ̄︶ ̄〃)