NamhaeSusan/go-arch-guard

GitHub: NamhaeSusan/go-arch-guard

一款基于 Go test 的架构护栏工具,通过静态分析与预设规则防止架构退化并支持 AI 编码代理。

Stars: 0 | Forks: 0

# go-arch-guard [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/16c258f4a5225426.svg)](https://github.com/NamhaeSusan/go-arch-guard/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/NamhaeSusan/go-arch-guard/branch/main/graph/badge.svg)](https://codecov.io/gh/NamhaeSusan/go-arch-guard) [![Go Report Card](https://goreportcard.com/badge/github.com/NamhaeSusan/go-arch-guard)](https://goreportcard.com/report/github.com/NamhaeSusan/go-arch-guard) [한국어](README.ko.md) 通过 `go test` 为 Go 项目提供架构防护规则,专为 AI 编码代理和快速迭代团队设计。 定义隔离、层方向、结构、命名和影响范围规则,在项目形态偏离时让常规测试失败。内置 **DDD**、**Clean Architecture**、**Layered**、**Hexagonal**、**Modular Monolith**、**Consumer/Worker**、**Batch** 和 **Event-Driven Pipeline** 预设,支持完全自定义架构模型。无需学习 CLI,无需单独配置文件。只需 Go 测试。 对 AI 代理友好: - `scaffold.ArchitectureTest(...)` 生成可直接复制的 `architecture_test.go` - `rules.RunAll(...)` 一次性运行推荐的规则集合 - `report.MarshalJSONReport(...)` 输出机器可读的违规记录,供机器人和修复循环使用 ## 为什么 架构退化通常源于少数几类广泛错误,而非深层的理论违规: - 跨域导入 - 隐藏的组成根 - 包位置漂移 - 破坏预期项目形态的命名 `go-arch-guard` 通过静态分析尽早捕获这些粗粒度错误。它设计得足够简单,便于 AI 代理生成和维护,同时仍对人类审查生成的边界有用。它不会尝试建模 Go 包内部的每个语义细节,如果 Go 自身已拒绝某些内容(如导入循环),那也不是主要目标。 ## 安装 ``` go get github.com/NamhaeSusan/go-arch-guard ``` ## 快速开始 ### 生成预设模板 对于 AI 代理或脚手架工具,生成可直接复制的 `architecture_test.go`: ``` import "github.com/NamhaeSusan/go-arch-guard/scaffold" src, err := scaffold.ArchitectureTest( scaffold.PresetHexagonal, scaffold.ArchitectureTestOptions{PackageName: "myapp_test"}, ) ``` `PackageName` 必须是有效的 Go 包标识符。不要盲目从连字符化的模块基名派生。 可用预设:`PresetDDD`、`PresetCleanArch`、`PresetLayered`、 `PresetHexagonal`、`PresetModularMonolith`、`PresetConsumerWorker`、`PresetBatch`、 `PresetEventPipeline`。 ### 推荐快捷方式 如果你想要推荐的规则集合而无需手动追加每个检查: ``` violations := rules.RunAll(pkgs, "", "") report.AssertNoViolations(t, violations) ``` 仅在需要非默认模型或严重性/排除选项时传递 `opts...`。 ### 每规则控制(DDD 示例) 对于单个检查的细粒度控制,手动组合它们: ``` func TestArchitecture(t *testing.T) { pkgs, err := analyzer.Load(".", "internal/...", "cmd/...") if err != nil { t.Log(err) } if len(pkgs) == 0 { t.Fatalf("no packages loaded: %v", err) } t.Run("domain isolation", func(t *testing.T) { report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", "")) }) t.Run("layer direction", func(t *testing.T) { report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", "")) }) t.Run("naming", func(t *testing.T) { report.AssertNoViolations(t, rules.CheckNaming(pkgs)) }) t.Run("structure", func(t *testing.T) { report.AssertNoViolations(t, rules.CheckStructure(".")) }) t.Run("blast radius", func(t *testing.T) { report.AssertNoViolations(t, rules.AnalyzeBlastRadius(pkgs, "", "")) }) } ``` 对于其他预设,使用对应的模型函数传入 `opts`: ``` m := rules.CleanArch() // or Layered(), Hexagonal(), ModularMonolith(), ConsumerWorker(), Batch(), EventPipeline() opts := []rules.Option{rules.WithModel(m)} rules.CheckDomainIsolation(pkgs, "", "", opts...) rules.CheckLayerDirection(pkgs, "", "", opts...) // ... same pattern for all Check* functions ``` ### 自定义模型 ``` m := rules.NewModel( rules.WithDomainDir("module"), rules.WithSharedDir("lib"), rules.WithSublayers([]string{"api", "logic", "data"}), rules.WithDirection(map[string][]string{ "api": {"logic"}, "logic": {"data"}, "data": {}, }), ) opts := []rules.Option{rules.WithModel(m)} ``` 运行: ``` go test -run TestArchitecture -v ``` 存在违规时的示例输出: ``` === RUN TestArchitecture/domain_isolation [ERROR] violation: domain "order" must not import domain "user" (file: internal/domain/order/app/service.go:5, rule: isolation.cross-domain, fix: use orchestration/ for cross-domain orchestration or move shared types to pkg/) --- FAIL: TestArchitecture/domain_isolation ``` 传入空字符串作为 `module` 和 `root` 可自动从已加载包中提取。如果无法确定模块,将发出 `meta.no-matching-packages` 警告。 ## 预设 | 预设 | 类型 | 子层 | 方向 | |------|------|------|------| | `DDD()` | 领域 | handler, app, core/model, core/repo, core/svc, event, infra | handler→app→core/*, infra→core/repo+core/model+event | | `CleanArch()` | 领域 | handler, usecase, entity, gateway, infra | handler→usecase→entity+gateway, infra→gateway+entity | | `Layered()` | 领域 | handler, service, repository, model | handler→service→repository+model | | `Hexagonal()` | 领域 | handler, usecase, port, domain, adapter | handler→usecase→port+domain, adapter→port+domain | | `ModularMonolith()` | 领域 | api, application, core, infrastructure | api→application→core, infrastructure→core | | `ConsumerWorker()` | 平铺 | worker, service, store, model | worker→service+model, service→store+model, store→model | | `Batch()` | 平铺 | job, service, store, model | job→service+model, service→store+model, store→model | | `EventPipeline()` | 平铺 | command, aggregate, event, projection, eventstore, readstore, model | command→aggregate+eventstore+model, aggregate→event+model, projection→event+readstore+model | 领域预设使用 `internal/domain/{name}/{layer}/` 布局。 平铺预设使用 `internal/{layer}/` 布局(无 domain 目录)。 详见 [预设详情](docs/presets.md) 获取完整的布局示意图和方向表。 ### 自定义模型选项 从 DDD 默认值开始,按需覆盖: ``` m := rules.NewModel( rules.WithDomainDir("module"), // internal/module/ instead of internal/domain/ rules.WithOrchestrationDir("workflow"), // internal/workflow/ rules.WithSharedDir("lib"), // internal/lib/ rules.WithSublayers([]string{"api", "logic", "data"}), rules.WithDirection(map[string][]string{ "api": {"logic"}, "logic": {"data"}, "data": {}, }), rules.WithRequireAlias(false), rules.WithRequireModel(false), ) ``` 所有模型选项: | 选项 | 描述 | |------|------| | `WithSublayers([]string{...})` | 识别的子层名称 | | `WithDirection(map[string][]string{...})` | 允许的导入方向矩阵 | | `WithPkgRestricted(map[string]bool{...})` | 必须不导入共享 pkg 的子层 | | `WithDomainDir("domain")` | 域顶级目录名称 | | `WithOrchestrationDir("orchestration")` | 编排顶级目录名称 | | `WithSharedDir("pkg")` | 共享包顶级目录名称 | | `WithRequireAlias(bool)` | 域根是否必须定义 alias.go | | `WithAliasFileName("alias.go")` | 别名文件名 | | `WithRequireModel(bool)` | 域是否必须包含 model 目录 | | `WithModelPath("core/model")` | 域 model 目录路径 | | `WithDTOAllowedLayers([]string{...})` | 允许 DTO 的层 | | `WithBannedPkgNames([]string{...})` | internal/ 下禁止的包名 | | `WithLegacyPkgNames([]string{...})` | 触发迁移警告的包名 | | `WithLayerDirNames(map[string]bool{...})` | 被视为“类似层”的目录名 | | `WithInterfacePatternExclude(map[string]bool{...})` | 跳过接口模式检查的层 | ## 隔离规则 `rules.CheckDomainIsolation(pkgs, module, root, opts...)` 防止域相互渗透。若无隔离,域 A 的变更可能无声地破坏域 B —— 这是 DDD 项目中最常见的意外耦合来源。 ### `isolation.cross-domain` 域不得直接导入其他域。 ``` // internal/domain/order/app/service.go package app import _ "myapp/internal/domain/user/app" // violation ``` ``` // use orchestration for cross-domain coordination package orchestration import ( "myapp/internal/domain/order" "myapp/internal/domain/user" ) ``` ### `isolation.cmd-deep-import` `cmd/` 仅能导入域根别名包,不得导入子包。 ``` // cmd/server/main.go import _ "myapp/internal/domain/order/app" // too deep import _ "myapp/internal/domain/order" // domain root only ``` ### `isolation.orchestration-deep-import` 编排仅能导入域根,保持耦合面最小。 ``` // internal/orchestration/checkout.go import _ "myapp/internal/domain/order/app" // too deep import _ "myapp/internal/domain/order" // domain root only ``` ### `isolation.pkg-imports-domain` 共享 `pkg/` 不得导入任何域 —— 应该是与域无关的。 ``` // internal/pkg/logger/logger.go import _ "myapp/internal/domain/order" // violation: pkg depends on domain ``` ### `isolation.pkg-imports-orchestration` 共享 `pkg/` 不得导入编排。 ### `isolation.domain-imports-orchestration` 域不得导入编排 —— 编排协调域,而非反过来。 ### `isolation.stray-imports-orchestration` 仅 `cmd/` 和编排本身可以依赖编排。 ### `isolation.stray-imports-domain` 非域内部包(除编排/cmd/pkg 外)不得导入域。 **导入矩阵:** | from | 域根 | 域子包 | 编排 | 共享 pkg | |------|------|--------|------|----------| | **同域** | 是 | 是 | 否 | 是 | | **其他域** | 否 | 否 | 否 | 是 | | **编排** | 是 | 否 | 是 | 是 | | **cmd** | 是 | 否 | 是 | 是 | | **共享 pkg** | 否 | 否 | 否 | 是 | ## 层方向规则 `rules.CheckLayerDirection(pkgs, module, root, opts...)` 防止层之间的反向依赖。若不强制方向,内部层(model、entity)会逐渐从外层累积导入,使其无法独立提取或测试。 ### `layer.direction` 导入必须遵循预设方向矩阵定义的方向。 ``` // DDD preset: core/svc may only import core/model package svc // internal/domain/order/core/svc/ import _ "myapp/internal/domain/order/app" // reverse direction import _ "myapp/internal/domain/order/core/model" // allowed ``` ### `layer.inner-imports-pkg` 标记为 `PkgRestricted` 的内层不得导入共享 `pkg/`。 这确保核心域逻辑不包含基础设施关注点。 ### `layer.unknown-sublayer` 检测域下不符合任何识别子层名称的目录。 ``` internal/domain/order/utils/ "utils" is not a recognized sublayer ``` ## 结构规则 `rules.CheckStructure(root, opts...)` 强制执行文件系统布局约定,防止在 vibe 编码过程中结构漂移。 ### `structure.internal-top-level` 仅允许指定目录存在于 `internal/` 顶层。 ``` // DDD: only domain/, orchestration/, pkg/ allowed internal/ domain/ allowed orchestration/ allowed pkg/ allowed config/ not in allowed list ``` ### `structure.banned-package` 阻止模糊的包名,这些名称会成为倾倒场。 默认禁止:`util`、`common`、`misc`、`helper`、`shared`、`services` ### `structure.legacy-package` 警告应迁移的包名:`router`、`bootstrap` ### `structure.misplaced-layer` 层目录(`app`、`handler`、`infra`)只能存在于域切片内,不能漂浮在 `internal/` 顶层。 ### `structure.middleware-placement` `middleware/` 必须位于 `internal/pkg/middleware/`,不得分散在域中。 ### `structure.domain-alias-exists`(DDD 仅) 每个域根必须定义 `alias.go` 文件作为其公共 API 表面。 ### `structure.domain-alias-package` 别名文件的包名必须与目录名匹配。 ### `structure.domain-alias-exclusive` 域根目录只能包含 `alias.go` —— 所有其他代码应位于子层。 ### `structure.domain-alias-no-interface` 别名文件不得直接定义接口 —— 这会泄露跨域契约。 ### `structure.domain-alias-contract-reexport` 别名文件不得重新导出子层(repo/svc)的类型 —— 这会在跨域创建隐藏依赖。 ### `structure.domain-model-required`(DDD 仅) 每个域必须包含 `core/model/` 目录,且至少有一个 Go 文件。 ### `structure.dto-placement` DTO 文件(`dto.go`、`*_dto.go`)只能存在于允许的层(handler、app)。 ## 命名规则 `rules.CheckNaming(pkgs, opts...)` 强制执行 Go 命名约定,保持代码库一致且便于 grep。 ### `naming.no-stutter` 导出的类型不得重复包名。 ``` package repo type RepoOrder struct{} // stutters: repo.RepoOrder type Order struct{} // clean: repo.Order ``` ### `naming.no-impl-suffix` 导出的类型不得以 `Impl` 结尾。请使用未导出类型替代。 ``` type OrderServiceImpl struct{} // Impl suffix type orderService struct{} // unexported ``` ### `naming.snake-case-file` 所有 Go 文件名必须为蛇形命名(snake_case)。 ``` OrderService.go violation order_service.go correct ``` ### `structure.repo-file-interface` `repo/`(或 `core/repo/`)中的文件必须包含与文件名匹配的接口。 ``` // order.go in repo/ must define: type Order interface { ... } // matches filename ``` ### `structure.repo-file-extra-interface` `repo/` 中的每个文件必须只定义一个接口。额外接口应拆分到独立文件。 ``` // repo/review.go type Review interface { Find() } // correct type Helper interface { Assist() } // violation: move to helper.go ``` ### `interface.too-many-methods` repo 接口不得超出 `WithMaxRepoInterfaceMethods` 设置的限制。默认禁用。 ``` rules.CheckNaming(pkgs, rules.WithMaxRepoInterfaceMethods(10)) ``` ``` // repo/review.go type Review interface { // 11 methods --- violation (max 10) } ``` ### `naming.no-layer-suffix` 文件名不得冗余地重复层名。 ``` // inside service/ directory: order_service.go "_service" suffix is redundant order.go correct ``` ### `structure.interface-placement`(DDD 仅) 仓库端口接口(名称以 `Repository` 或 `Repo` 结尾)必须定义在 `core/repo/`,不得分散在多层。消费者定义的接口(Go 惯用法,即包声明其使用的最小接口)可放置在任意位置:`handler/`、`app/`、`svc/` 等。 同时标记 `type X = otherdomain.Repo` 的别名(跨域重新导出仓库接口)应位于 `orchestration/`。 ### `testing.no-handmock` 测试文件不得定义手工编写的 mock/fake/stub 结构体及其方法。应使用 mockery 或其他生成工具。 ### `naming.type-pattern-mismatch`(平铺预设) 匹配 TypePattern 前缀的文件必须定义对应的类型。 ``` // worker/worker_order.go must define: type OrderWorker struct{} // expected type SomethingElse struct{} // expected OrderWorker ``` ### `naming.type-pattern-missing-method`(平铺预设) 匹配 TypePattern 的类型必须包含所需方法。 ``` type OrderWorker struct{} // missing Process method --- violation func (w *OrderWorker) Process(ctx context.Context) error { ... } // correct ``` ## 接口模式规则 `rules.CheckInterfacePattern(pkgs, opts...)` 强制执行 Go 接口最佳实践:私有实现、仅 `New()` 构造函数、接口返回类型、每个包一个接口。 ### `interface.exported-impl` 导出的结构体不得实现接口 —— 应将实现类型设为未导出,以防止消费者依赖具体类型。 ``` type RepositoryImpl struct{ db *sql.DB } // exported struct implements interface type repository struct{ db *sql.DB } // unexported --- correct ``` ### `interface.constructor-name` 构造函数必须命名为 `New`,而非 `NewXxx` 变体。这在所有包中强制一致的工厂模式。 ``` func NewRepository(db *sql.DB) Repository // NewXxx not allowed func New(db *sql.DB) Repository // correct ``` ### `interface.constructor-returns-interface` `New()` 必须返回接口,而非具体类型。这确保调用方依赖契约而非实现。 ``` func New(db *sql.DB) *repository // returns concrete type func New(db *sql.DB) Repository // returns interface --- correct ``` ### `interface.single-per-package` 每个包最多一个导出接口(警告)。单个包中存在多个接口通常意味着职责过多。 排除层由 `InterfacePatternExclude` 按预设控制(入口点、model、event、pkg)。 ### `interface.cross-domain-anonymous` 检测在所属域之外(且不在指定编排层)声明的匿名接口,其方法签名触及其他域的类型。默认严重级别为 **Error**。 该规则强制约定:**跨域抽象应由编排包拥有**,而非任意布线代码。 在 `cmd/`(或 `internal/pkg/`)中声明与域类型相关的匿名接口会创建不受控的跨域表面;此类适配器/抽象应位于 `internal/orchestration/`。 ``` // flagged: cmd/ declares inline interface that abstracts a domain type package main import "example.com/p/internal/domain/user" type adapter struct { repo interface { // ← cross-domain anonymous in cmd/ GetByID(ctx context.Context, id string) (*user.User, error) } } ``` ``` // not flagged: same shape but inside the orchestration layer where // cross-domain coordination is by design package orchestration import "example.com/p/internal/domain/user" type userInfoAdapter struct { repo interface { // ← anonymous, but orchestration is exempt GetByID(ctx context.Context, id string) (*user.User, error) } } ``` 修复方法是 **将适配器移至编排包**,并让布线代码调用编排构造函数,而非自行声明接口。 跳过: - 测试文件(`_test.go`),其中 mock/fake 固件自然采用此形态 - 空接口(`interface{}`)及无方法声明的接口 - 嵌入接口类型(如 `interface { io.Reader }`) - 同域引用(`internal/domain/X` 中匿名接口引用同域类型) - `internal//` 包中的类型 —— 编排是指定的跨域协调层 - 无 domainDir 的模型(平铺布局如 ConsumerWorker、Batch、EventPipeline) ### `interface.container-only` 检测到接口仅在结构体字段类型中使用 —— 从未作为函数参数或返回类型。默认严重级别为 **Warning**。 这是 vibe 编码的异味:接口被用作值容器而非抽象。常见原因是布线层需要持有某个值,而其具体类型未暴露(例如 `alias.go` 重新导出构造函数但未导出类型),于是开发者声明本地接口仅为字段提供类型。 ``` // flagged: container-only — never used as parameter or return type userRepo interface { GetByID(id string) string } type holder struct { r userRepo // only usage } ``` ``` // not flagged: legitimate consumer-defined interface type userRepo interface { GetByID(id string) string } func newHolder(r userRepo) *holder { // used as parameter → real abstraction return &holder{r: r} } ``` 跳过: - 测试文件(`_test.go`),其中 mock/fake 固件自然采用此形态 - 类型别名(`type Foo = pkg.Foo`) - 结构体中的嵌入字段(匿名嵌入) - 未使用的接口(属于不同类别) 该规则不规定修复方式。两种常见解决: 1. 从 `alias.go` 重新导出具体类型,使字段可直接持有该类型。 2. 重写布线逻辑,使值成为函数内的局部变量,而非跨函数共享的结构体字段。 严重性可通过 `WithSeverity(Error)` 升级为 Error,若项目希望将此异味作为硬规则。 ## 影响范围分析 `rules.AnalyzeBlastRadius(pkgs, module, root, opts...)` 通过 IQR 统计离群值检测内部包的高耦合度。默认严重级别为 Warning。跳过包含少于 5 个内部包的项目 | 规则 | 含义 | |------|------| | `blast.high-coupling` | 包具有统计上显著的传递依赖数 | | 指标 | 定义 | |------|------| | Ca(入度耦合) | 导入该包的其他包数量 | | Ce(出度耦合) | 该包导入的其他包数量 | | 不稳定性 | Ce / (Ca + Ce) | | 传递依赖 | 通过 BFS 反向可达的完整集合 | ## 选项 ### 严重性 ``` // Log violations without failing the test rules.CheckDomainIsolation(pkgs, "", "", rules.WithSeverity(rules.Warning)) ``` ### 排除路径 ``` // Skip subtrees during migration rules.CheckDomainIsolation(pkgs, "", "", rules.WithExclude("internal/legacy/..."), ) ``` 模式为项目相对路径,使用正斜杠。`...` 匹配根目录及其所有后代。 ## TUI 查看器 在终端 UI 中可视化项目包结构与依赖关系。 ``` go run github.com/NamhaeSusan/go-arch-guard/cmd/tui . ``` 特性:健康状态树着色、导入/反向依赖/耦合指标、违规详情、搜索/过滤(`/`)、键盘导航。 ## API 参考 | 函数 | 描述 | |------|------| | `analyzer.Load(dir, patterns...)` | 加载 Go 包用于分析 | | `rules.CheckDomainIsolation(pkgs, module, root, opts...)` | 跨域边界检查 | | `rules.CheckLayerDirection(pkgs, module, root, opts...)` | 域内方向检查 | | `rules.CheckNaming(pkgs, opts...)` | 命名约定检查 | | `rules.CheckStructure(root, opts...)` | 文件系统结构检查 | | `rules.AnalyzeBlastRadius(pkgs, module, root, opts...)` | 耦合离群检测 | | `rules.CheckInterfacePattern(pkgs, opts...)` | 接口模式最佳实践 | | `rules.RunAll(pkgs, module, root, opts...)` | 运行内置推荐规则集合 | | `report.AssertNoViolations(t, violations)` | 在测试中因 Error 违规失败 | | `report.BuildJSONReport(violations)` | 构建机器可读的 JSON 报告 | | `report.MarshalJSONReport(violations)` | 序列化机器可读的 JSON 报告 | | `report.WriteJSONReport(w, violations)` | 写入机器可读的 JSON 报告 | | `scaffold.ArchitectureTest(preset, opts)` | 生成预设专用的 `architecture_test.go` 模板 | | `rules.DDD()` | DDD 架构模型(默认) | | `rules.CleanArch()` | Clean Architecture 模型 | | `rules.Layered()` | Spring 风格的分层模型 | | `rules.Hexagonal()` | 端口与适配器模型 | | `rules.ModularMonolith()` | 基于模块的层次模型 | | `rules.ConsumerWorker()` | Consumer/Worker 平铺布局模型 | | `rules.Batch()` | Batch 平铺布局模型 | | `rules.EventPipeline()` | 事件溯源 / CQRS 平铺布局模型 | | `rules.CheckTypePatterns(pkgs, opts...)` | 基于 AST 的类型模式强制 | | `rules.NewModel(opts...)` | 自定义模型构建器 | | `rules.WithModel(m)` | 将自定义模型应用于检查 | | `rules.WithSeverity(rules.Warning)` | 降级为 Warning | | `rules.WithExclude("path/...")` | 跳过子树 | | `rules.WithMaxRepoInterfaceMethods(10)` | 限制 repo 接口方法数量 | ## 机器可读的 JSON 输出 用于 CI、机器人或自动化修复循环,以相同格式输出违规记录: ``` import "github.com/NamhaeSusan/go-arch-guard/report" data, err := report.MarshalJSONReport(violations) if err != nil { return err } fmt.Println(string(data)) ``` ## Claude Code 插件 ``` /plugin marketplace add NamhaeSusan/go-arch-guard /plugin install go-arch-guard@go-arch-guard-marketplace ``` ## 外部导入卫生 `go-arch-guard` 仅检查**项目内部**导入。外部依赖的卫生应由 AI 指令和代码审查保证。参考 [DDD 外部导入约束](README.ko.md#외부-import-위생--이-라이브러리가-아닌-ai-도구-지침으로-강제) 获取可复制的模板。 ## 许可证 MIT
标签:AI编码代理, CI集成, DDD, EVTX分析, Go, Go测试, Ruby工具, 事件驱动流水线, 云安全监控, 代码库结构, 代码规范, 六角架构, 包隔离, 命名规范, 批处理, 文档结构分析, 日志审计, 架构分层, 架构守护, 架构规则, 模块化单体, 测试驱动, 消费者工作器, 清洁架构, 静态分析