跳到主要内容

解决Nginx多SSL配置中未匹配域名的默认回退问题

· 阅读需 2 分钟

问题现象

我给 nginx 配置了多个server,分别设置了 ssl 代理,但是当其中一个服务未启动是,会访问到第一个 server 的内容。

server {
listen 443 ssl;
server_name a.domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://xxxx:10010;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name b.domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://xxxx:10086;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

当 10086 端口的服务未启动时,访问 https://b.domain.com 时,会出现 10010端口的服务的内容。 也就是 nginx 只会访问第一个 server 的内容。经过一番查找,发现是因为没有设置 default_server

解决方案

添加一个下面的 server 配置

server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
return 444; # 立即关闭连接
}

问题剖析

Nginx 请求匹配机制

  1. 端口监听优先级:Nginx优先匹配 listen指令相同的配置
  2. 域名精确匹配:在相同监听端口中,server_name最精确匹配的配置生效
  3. 默认回退机制:当没有匹配的server_name 时,自动选择:
    • 第一个定义的 server 块(未显式声明 default_server 时)
    • 标记为default_server的配置块

问题根源

当后端服务不可达时,Nginx 的请求处理流程:

  1. 客户端请求 b.domain.com
  2. Nginx正确匹配到 b.domain.com 的 server 块
  3. 尝试代理到http://xxxx:10086时发现连接失败
  4. 错误配置导致匹配降级,回退到默认 server 块(第一个定义的 a.domain.com

Nginx 反向代理 WebSocket 服务

· 阅读需 1 分钟
# 反向代理
server {
server_name domain.com;
listen 443 ssl;

# 通过 acme.sh 进行签署,具体见:https://blog.wangzhy.com/ssl#acmesh-%E7%AD%BE%E7%BD%B2%E8%AF%81%E4%B9%A6
ssl_certificate /etc/nginx/ssl/xxxx/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/xxxx/domain.com.key;

# SSL 增强配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;


location / {
proxy_pass http://host.docker.internal:9001;
# 基础代理头设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 连接数限制
proxy_http_version 1.1; # 明确指定HTTP1.1
proxy_buffers 8 16k; # 缓冲区优化
proxy_buffer_size 32k;

# WebSocket 专用设置
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# 调整超时时间(秒)
proxy_read_timeout 86400; # 保持长连接
proxy_send_timeout 86400;
}
}

TypeScript interface 关键字介绍

· 阅读需 3 分钟

interface 用于定义对象类型函数签名类结构等。

基础语法

interface User {
name: string;
age: number;
isAdmin?: boolean; // 可选属性(Optional)
readonly id: number; // 只读属性
}
  • ? 表示非必须
  • readonly 表示只读

函数类型

  1. 定义了一个函数签名,参数是 source keyword,返回值是 boolean 类型。
  2. 声明了一个 search 常亮,并赋值了一个箭头函数。
interface SearchFunc {
(source: string, keyword: string): boolean;
}

// src, kw 名称不需要与函数类型定义中一致,但参数类型需要一致。
const search: SearchFunc = (src, kw) => src.includes(kw);

使用场景

约束回调函数

interface ClickHandler {
(event: MouseEvent, context: HTMLElement): void;
}

const handleClick: ClickHandler = (e, element) => {
console.log("Clicked on:", element.tagName);
};

统一 API 请求函数

interface ApiRequest {
(url: string, params: Record<string, string>): Promise<Response>;
}

const fetchData: ApiRequest = async (path, data) => {
return await fetch(path, { body: JSON.stringify(data) });
};

高阶函数参数

function higherOrderFunc(callback: SearchFunc) {
callback("Hello", "H"); // 调用时必须传入符合签名的函数
}

higherOrderFunc((s, k) => s.startsWith(k));

可索引类型

// 数组类型
interface StringArray {
[index: number]: string; // 索引为数字,值为字符串
}

// 字典类型
interface NumberDictionary {
[key: string]: number; // 键为字符串,值为数字
length: number; // 明确声明的属性必须符合索引签名
}

使用案例:

// 用索引获取数组元素
interface StudentArray {
[index: number]: string;
}

let studentArr: StudentArray = ["Bob", "Fred"];
let student1: string = studentArr[0]; // 'Bob'
let student2: string = studentArr[1]; // 'Fred'

// 用索引获取对象属性
interface StudentObject {
[key: string]: number;
}

let studentObj: StudentObject = {};
studentObj['Bob'] = 1;
studentObj['Fred'] = 2; // { Bob: 1, Fred: 2 }

继承与扩展

interface Animal {
name: string;
}

interface Dog extends Animal {
breed: string;

bark(): void;
}

// 多重继承
interface SuperDog extends Dog {
fly(): void;
}

类实现接口

interface ClockInterface {
currentTime: Date;

setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();

setTime(d: Date) {
this.currentTime = d;
}
}

混合类型

interface Counter {
(start: number): string; // 函数签名
interval: number; // 属性
reset(): void; // 方法
}

// 实现示例
function createCounter(): Counter {
let counter = function (start: number) {
} as Counter;
counter.interval = 5;
counter.reset = () => {
};
return counter;
}

接口合并

interface Box {
width: number;
}

interface Box {
height: number;
}

// 合并后等价于:
interface Box {
width: number;
height: number;
}

动态扩展接口

interface Window {
myCustomProp: string; // 扩展全局 Window 类型
}

泛型接口

interface ApiResponse<T> {
code: number;
data: T;
message?: string;
}

// 使用
const userResponse: ApiResponse<User> = {
code: 200,
data: {name: "Alice", age: 30}
};

环境变量替换

· 阅读需 1 分钟

可以在 Dockerfile、docker-compose.yml 中使用环境变量 替换${}。它允许你在构建镜像时动态地设置变量的值。

${} 语法

基本形式: ${VAR}

environment:
- APP_PORT=${APP_PORT}

默认值: ${VAR:-default}

HOST_PORT 未设置时,采用默认值 8080

ports:
- "${HOST_PORT:-8080}:80"

强制: ${VAR:?error}

DATA_PATH 如果未设置,会报错并退出。

volumes:
- "${DATA_PATH:?DATA_PATH must be set}:/data"

组合: ${VAR:-${DEFAULT_VALUE}}

environment:
- DB_HOST=${DB_HOST:-${FALLBACK_DB_HOST}}

环境变量设置

1. Shell 环境变量

export APP_PORT=8080

2. .env 文件

docker-compose 会自动加载项目根目录下的 .env 文件。

可以通过 --env-file 指定 .env 文件。 例如:

docker compose --env-file custom.env up -d

3. 命令行通过 -e --env 参数指定

Dockerfile

docker -e APP_PORT=8080 run myapp

docker-Compose.yml

docker compose up -d  --env APP_PORT=8080

go:embed 指令

· 阅读需 2 分钟

[GoLang官方提案]https://go.googlesource.com/proposal/+/master/design/draft-embed.md

//go:embed 支持嵌入静态文件。

  1. 对于单个文件,可以嵌入为字符串和 byte slice
  2. 多个文件,支持嵌入为新的文件系统 FS
  3. 只支持嵌入为string, byte sliceembed.FS三种类型,这三种类型的别名(alias)和命名类型(如type S string)都不可以

嵌入字符串

//go:embed hello.txt
var s string

嵌入为 byte slice

//go:embed hello.txt
var b []byte

嵌入为fs.FS

基本使用

//go:embed hello.txt
var f embed.FS

支持同一变量上嵌入多个文件

//go:embed hello1.txt
//go:embed hello2.txt
var f embed.FS

func main() {
data, _ := f.ReadFile("hello.txt")
fmt.Println(string(data))
data, _ = f.ReadFile("hello2.txt")
fmt.Println(string(data))
}

匹配模式

go:embed dirpath 次文件夹下的除了 ._ 开头的文件/文件夹都会被嵌入(递归)。

如果要嵌入._开头的文件/文件夹,需要使用 go:embed dirpath/*

注意:这个只能嵌入 dirpath 目录下的 ._开头的文件/文件夹。子文件夹下的 ._ 开头的文件/文件夹是不会嵌入。

使用

将前端静态文件打包进 go 程序

嵌入静态文件

//go:embed frontend/out/*
var buildFS embed.FS

输出日志,检查文件是否被正确嵌入

// 打印嵌入的文件系统内容,用于调试
fs.WalkDir(buildFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
log.Printf("嵌入文件系统中有: %s", path)
}
return nil
})

静态文件服务

// 创建一个子文件系统,将嵌入的文件系统 buildFS 中的 frontend/out 目录作为新文件系统的根目录
contentFS, err := fs.Sub(buildFS, "frontend/out")
if err != nil {
log.Fatalf("无法获取嵌入的前端文件: %v", err)
}

// 创建一个文件服务处理器
fileServer := http.FileServer(http.FS(contentFS))

// 为所有路径提供服务
r.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 打印请求路径,帮助调试
log.Printf("请求文件: %s", r.URL.Path)
fileServer.ServeHTTP(w, r)
}))

Go语言JSON结构体标签

· 阅读需 2 分钟

结构体标签详解

type User struct {
ID int `json:"id"` // 字段重命名(服务端返回"id"字段)
Name string `json:"name"` // 保持字段原名
Email string `json:"email,omitempty"` // 空值时自动隐藏(避免空字符串污染JSON)
Secret string `json:"-"` // 彻底隐藏敏感字段(如密码)
Created time.Time `json:"created_at"` // 时间类型自动格式化为RFC3339字符串
}

序列化最佳实践

// 带缩进的友好格式输出
data, _ := json.MarshalIndent(user, "", " ")
/*
{
"id": 1,
"name": "Alice",
"created_at": "2023-09-15T10:00:00Z"
}
*/

// 原始紧凑格式(适合网络传输)
binaryData, _ := json.Marshal(user) // []byte类型数据

反序列化注意事项

var newUser User
err := json.Unmarshal(data, &newUser)

// 处理日期字段(需显式转换)
type CustomTime struct {
time.Time
}
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
// 自定义日期解析逻辑...
}

常见问题排查

  1. 字段丢失:检查结构体是否导出(首字母大写)
  2. 时间格式错误:使用time.RFC3339格式字符串
  3. 零值问题:结合omitempty与指针类型*string
  4. 循环引用:避免结构体嵌套循环

高级技巧

// 动态字段处理
type FlexibleStruct struct {
Extra map[string]interface{} `json:"-"` // 收集未定义字段
}

// 条件序列化
func (u User) MarshalJSON() ([]byte, error) {
// 自定义序列化逻辑...
}

动态类型处理

// 解析未知结构 JSON
data := []byte(`{"name":"Bob","age":30}`)
var result map[string]interface{}
json.Unmarshal(data, &result)
fmt.Println(result["name"].(string))

// 部分解析(使用匿名结构)
var partial struct {
Name string `json:"name"`
}
json.Unmarshal(data, &partial)

自定义序列化

// 实现 json.Marshaler 接口
type CustomTime time.Time

func (ct CustomTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, time.Time(ct).Format("2006/01/02"))), nil
}

// 实现 json.Unmarshaler 接口
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
// 自定义解析逻辑...
}

处理 HTML 转义

buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false) // 禁用 &, <, > 转义
enc.Encode(data)

Go 内存分配机制

· 阅读需 2 分钟

问题引入

在阅读 New-API 源码的时候,发现在 struct 中定义一个 *string 类型的字段

type ClaudeMediaMessage struct {
Type string `json:"type,omitempty"`
Text *string `json:"text,omitempty"`
Model string `json:"model,omitempty"`
Source *ClaudeMessageSource `json:"source,omitempty"`
Usage *ClaudeUsage `json:"usage,omitempty"`
StopReason *string `json:"stop_reason,omitempty"`
PartialJson *string `json:"partial_json,omitempty"`
Role string `json:"role,omitempty"`
Thinking string `json:"thinking,omitempty"`
Signature string `json:"signature,omitempty"`
Delta string `json:"delta,omitempty"`
// tool_calls
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input any `json:"input,omitempty"`
Content json.RawMessage `json:"content,omitempty"`
ToolUseId string `json:"tool_use_id,omitempty"`
}

func (c *ClaudeMediaMessage) SetText(s string) {
c.Text = &s
}

struct 中定义一个 *string 类型的字段的目的

  1. 可以区分此字段是否被设置,或者被设置为 ""

    • 原理是:*string 是一个字符串指针,默认值是 nil, 而 string 的默认值是 ""
  2. 在序列换 JSON 的时候,如果字段是 *string 类型,且值为 nil,则在序列化时会被忽略。

  3. API 设计时,可能很好的表达出可选字段。

  4. 避免频繁出现空字符串。

    • var s string , s 会被初始化为 "",变量没有逃逸,则会被分配在栈上,底层实现是 StringHeader, 大小 16 字节(指向底层字节数组的指针(8 字节,64 位系统)、字符串长度(8 字节,64 位系统))
    • var sp *string , sp 默认值是 nil, 声明时立即分配内存(通常是在栈上),大小 8 字节(由操作系统决定,64位,8字节)

文章引用

Go语言时间处理

· 阅读需 1 分钟

时间格式化基础

自定义格式输出

Go使用特定时间戳(2006-01-02 15:04:05)作为布局字符串:

t := time.Now()
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2023-10-08 14:30:45
fmt.Println(t.Format("2006/01/02 15:04:05")) // 2023/10/08 14:30:45
fmt.Println(t.Format("2006.01.02 15:04:05")) // 2023.10.08 14:30:45
fmt.Println(t.Format("2006年01月02日 15时04分05秒")) // 2023年10月08日 14时30分45秒

预定义标准格式

Go内置多种标准格式,直接使用time包常量:

fmt.Println(time.Now().Format(time.RFC3339))  // 2023-10-08T14:30:45+08:00
fmt.Println(time.Now().Format(time.RFC1123)) // Mon, 08 Oct 2023 14:30:45 CST

字符串解析实践

yyyyMM格式解析

处理无分隔符的日期字符串时需注意布局字符串格式:

func parseYearMonth(str string) (int, time.Month) {
t, err := time.Parse("200601", str) // 布局字符串必须使用"200601"
if err != nil {
log.Fatal("解析失败:", err)
}
return t.Year(), t.Month()
}

// 使用示例
year, month := parseYearMonth("202310")
fmt.Printf("%d年%s月", year, month) // 2023年October月

= 与 := 的区别

· 阅读需 2 分钟

基础用法

= 赋值操作符

  1. 仅用于赋值,不声明变量
  2. 可以在任何代码快中使用
var a int
a = 100

:= 短变量声明

  1. 声明变量的同时赋值
  2. 只能在函数内部使用
  3. 在给多个变量的时候赋值的时候,至少要定义一个新变量
a := 100
a,b := 200,300

使用案例

闭包

func closureDemo(){
for i:=0; i< 3;i++ {
go func(){
fmt.Print(i," ") // 闭包
}()
}
}

这段代码可能会输出 3 3 3,因为 i 是在闭包外部定义的变量,所有的 goroutine 都共享同一个 i 变量。

func closureDemo(){
for i:=0; i< 3;i++ {
val :=i
go func(){
fmt.Print(val," ")
}()
}
}

使用局部变量可以避免闭包带来的问题。每次循环都会创建一个新的变量。

i 变量定义在中,val 变量定义在中。

func closureDemo(){
for i:=0; i< 3;i++ {
go func(v int){
fmt.Print(v," ")
}(i)
}
}

通过参数将 i 的值传递给闭包函数,确保每个 goroutine 都有自己的 i 值。

并发下的安全赋值/原子操作

func main() {
var counter *int64 = new(int64)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(v *int64) {
*v += 1
wg.Done()
}(counter)
}
wg.Wait()
fmt.Println("Counter:", *counter)
}

输出的值小于 1000

func concurrentAssignment() {
var counter int64
var wg sync.WaitGroup

// 使用原子操作保证并发安全
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
atomic.AddInt64(&counter, 1) // 使用=修改共享变量
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)

// 使用通道安全传递值
ch := make(chan int)
go func() {
val := <-ch // 使用:=接收新值
fmt.Println("Received:", val)
}()
ch <- 42 // 使用=发送值
}

使用 atomic.AddInt64 来实现原子操作,确保在并发环境下对 counter 的安全修改。