跳到主要内容

解决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

环境变量替换

· 阅读需 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

初始化结构体指针

· 阅读需 2 分钟
type User struct {
ID int
Name string
}

直接结构体指针(字面量写法) 🔔推荐

使用 & 符号直接创建结构体指针,并初始化字段. 推荐使用

优点:

  1. 字段与值显式对应,清晰直观
  2. 自动推导类型,减少冗余代码
  3. 字段顺序无关,结构体修改时更安全
u := &User{
ID: 1,
Name: "Alice",
}

先 new 后赋值

// 1. 分配内存,返回一个指向零值的指针
u := new(User)

// 2. 手动赋值字段
u.ID = 1
u.Name = "Alice"

按顺序初始化 不推荐

user := &User{2, "Alice"}

缺点:

  1. 必须严格匹配字段顺序,调整结构体字段顺序会导致初始化错误
  2. 可读性差,尤其当字段数量多或类型相似时容易出错

Go语言时间处理

· 阅读需 2 分钟

时间格式化基础

自定义格式输出

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月

Go语言JSON结构体标签

· 阅读需 3 分钟

结构体标签详解

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)

TypeScript interface 关键字介绍

· 阅读需 4 分钟

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}
};