W3
02合约开发入门9 个步骤4 个小节预计 35-45 分钟v1 · 更新于 2026-09-19

Solidity 语法基础:合约结构、数据类型与权限控制

系统讲透 Solidity 写智能合约必备的语法骨架:pragma 版本声明的含义与 ^ / ~ 范围规则;状态变量、局部变量、构造函数、常量与 immutable 的区别;值类型(uint 系列、address、bool、定长 bytes、枚举)与引用类型(数组、struct、mapping)及 memory / storage / calldata 三种数据位置;public / external / internal / private 四种可见性与 view / pure 修饰;modifier 修饰器与 onlyOwner 权限模式;event 事件与 indexed 参数;合约收 ETH 的 receive / fallback 与 call 转账;最后是 interface、library 与继承 is,学完即可读懂绝大多数合约源码。

Solidity 语法基础:合约结构、数据类型与权限控制
⚠️
本章要点提示

语法层面的几个「默认行为」直接关联资金安全:不写可见性的函数在旧版本中默认 public;用错 storage / memory 可能意外修改链上状态;只靠 private 隐藏数据并不等于加密(链上数据人人可读);owner 权限如果遗漏校验,任何人都能调用管理函数。学语法时请同步记住每个关键字的安全含义。

小节 01

合约骨架与版本声明

步骤 12
01

pragma:为什么每个合约第一行都要声明版本

登录记录进度

Solidity 源文件第一行通常是 pragma solidity ^0.8.20;。它不改变代码行为,而是声明这段代码要求的编译器版本范围,防止用不兼容的编译器编译出意外结果。

版本号遵循语义化版本 主版本.次版本.补丁: - ^0.8.20:最常见,表示「不低于 0.8.20,且不跨越到 0.9.0」,即允许 0.8.x 的新版本。 - ~0.8.20:只允许补丁升级(0.8.20 ~ 0.8.x 中更高版本,不进 0.9)。

  • >=0.8.0 <0.9.0:最显式的写法。 - 不写符号直接 0.8.20:锁定唯一版本。

0.8.0 是一个关键分水岭:从 0.8.0 起编译器内置了整数溢出/下溢检查(之前需要 SafeMath 库),新项目应直接使用 0.8.x。

pragma:为什么每个合约第一行都要声明版本
pragma:为什么每个合约第一行都要声明版本|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

审计合约时先看 pragma:如果版本早于 0.8.0 且没用 SafeMath,就要立刻检查所有算术运算的溢出风险。

02

合约的骨架:状态变量、构造函数与函数

登录记录进度

一个最简单的合约由三部分组成:

contract Counter {
    uint256 public count;            // 状态变量:永久存储在链上
    address public owner;            // 部署者地址

    constructor() {                  // 部署时只执行一次
        owner = msg.sender;
    }

    function increment() external {  // 函数:被调用时执行
        count += 1;
    }
}

状态变量存储在链上 Storage 中,写一次就要付 Gas,所有节点永久保存;局部变量只在函数执行期间存在于内存,不花存储 Gas。还有两个特殊关键字:constant(编译期常量,如 uint256 public constant FEE = 10;)和 immutable(部署时在构造函数里赋值一次后不可改),二者都不占用 storage slot,能显著省 Gas。

合约的骨架:状态变量、构造函数与函数
合约的骨架:状态变量、构造函数与函数|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

凡是部署后永不变的值(地址、费率分母)尽量用 immutable/constant,长期能省下可观的部署与调用 Gas。

小节 02

数据类型与数据位置

步骤 34
03

值类型:uint、address、bool 与 bytes

登录记录进度

Solidity 的值类型复制时传递的是值本身: - 整数uint256(0 到 2²⁵⁶-1 的无符号整数,最常用)、int256(有符号),还有 uint8/uint128 等按 8 位递增的短位宽。

金额与数量默认一律用 uint256,短位宽主要用于 struct 里打包省 Gas。 - address:20 字节地址。address payable 额外拥有 .transfer() / .send() 能力;常用方法有 .balance(查 ETH 余额)和 .call{value: x}("")

  • bool:true / false。 - 定长字节:bytes1 ~ bytes32(bytes32 常用于哈希、Merkle 根)。另有 string 与变长 bytes,属于引用类型。

  • enum:自定义枚举,如 enum Status { Pending, Shipped, Done },底层就是 uint8。

注意:Solidity 不支持浮点数(早期版本),所有金额都用最小单位的整数表示(ETH 用 wei,1 ETH = 10¹⁸ wei),ERC-20 用对应 decimals 的最小单位。

值类型:uint、address、bool 与 bytes
值类型:uint、address、bool 与 bytes|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
🚫
避坑提醒:

永远不要用浮点数处理资金;乘除顺序上先乘后除可以减小精度损失,必要时使用成熟的定点数学库。

04

引用类型与数据位置:memory、storage、calldata

登录记录进度

数组(uint256[])、结构体 struct、映射 mapping 和变长 bytes/string 属于引用类型,使用时必须声明数据位置(Data Location): - storage:链上持久存储,最贵。

状态变量默认就是 storage;函数参数中只有 internal 函数能用 storage 引用。 - memory:函数执行期间的临时内存,函数结束即释放。函数的局部引用变量与参数常用 memory。

  • calldata:调用者传入的原始数据区,只读、不可修改,external 函数的数组/struct 参数首选 calldata,比 memory 更省 Gas。

典型错误:在函数里写 uint256[] storage arr = someStateArray; 然后修改 arr,会真的改动链上状态;而 mapping 没有长度、不能遍历、不能作为函数返回值(要用迭代器模式或额外数组记录 key)。

引用类型与数据位置:memory、storage、calldata
引用类型与数据位置:memory、storage、calldata|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
🚫
避坑提醒:

把 memory 误写成 storage 指向状态变量,是「函数意外改了合约数据」类漏洞的常见来源,写引用赋值时务必确认数据位置。

小节 03

函数、权限与事件

步骤 57
05

函数可见性与 view / pure

登录记录进度

函数必须显式声明可见性(0.5 之后不再有默认 public): - external:只能从合约外部(或其他合约)调用,外部调用时参数直接走 calldata,最省 Gas;内部需用 this.func()

  • public:内外都能调用,编译器会自动生成同名的外部接口。 - internal:仅合约内部与子合约可调用。 - private:仅本合约内部,连子合约都不可见。

两个状态修饰词:view 表示只读不修改状态(可读取链上数据),pure 表示连状态都不读,只依赖参数做纯计算(如求和、工具库)。view/pure 函数在链下调用不花 Gas(节点本地计算即可),但被链上交易调用时仍要付费。

函数可见性与 view / pure
函数可见性与 view / pure|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
🚫
避坑提醒:

private 只阻止其他合约「调用」,链上数据对全世界仍然可见——永远不要把密码、密钥、后门开关藏进「private 变量」,任何人都能用区块浏览器的 Storage 读取功能看到。

06

modifier 与 onlyOwner:最经典的权限模式

登录记录进度

修饰器(modifier)是在函数执行前后插入检查逻辑的语法,最经典的用途是权限控制: solidity address public owner; modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; // 下划线代表「被修饰函数的函数体」插入的位置 } function setFee(uint256 f) external onlyOwner { fee = f; } 调用 setFee 时会先执行 require,条件不满足就整体回滚;_; 放在最后表示「先检查后执行」(Checks-Effects-Interactions 模式常用)。

OpenZeppelin 的 Ownable / Ownable2Step 已经封装好了 owner 转移与 onlyOwner,新项目应直接继承而不是自己手写。

另有 nonReentrant(防重入,下一章详解)等常用 modifier。

modifier 与 onlyOwner:最经典的权限模式
modifier 与 onlyOwner:最经典的权限模式|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

任何「只有管理员能调用」的函数都应挂权限修饰器;审计时会逐个检查这类函数是否真的做了校验。

07

事件 Event:合约对外「发通知」的唯一方式

登录记录进度

事件(event)不会把数据写进合约 storage(更便宜),而是写进交易收据的日志区,外部应用通过监听事件感知链上变化: solidity event Transfer(address indexed from, address indexed to, uint256 value); emit Transfer(msg.sender, to, amount); 关键点: - 事件参数最多给 3 个indexed,indexed 参数会进入布隆过滤器,前端可以按地址/主题高效过滤订阅(如「只监听与我地址相关的 Transfer」)。

  • 非 indexed 参数以 ABI 编码存入日志,可读但不能直接按它过滤。 - ERC-20 标准强制要求 Transfer / Approval 事件,区块浏览器正是靠解析这些事件来显示你的代币流水。 - 合约自己不能读取历史事件——日志是给链下世界看的,需要被合约使用的数据必须同时存进状态变量。
事件 Event:合约对外「发通知」的唯一方式
事件 Event:合约对外「发通知」的唯一方式|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

所有关键状态变更(转账、权限变更、参数调整)都应 emit 事件,这既方便前端,也方便事后审计与监控告警。

小节 04

与外部交互和代码复用

步骤 89
08

合约怎么收 ETH:receive、fallback 与 call

登录记录进度

一个合约想接收 ETH,需要定义 receive()fallback(): - receive() external payable { }:当交易不带 calldata(纯转账)时触发。

  • fallback() external payable { }:当调用的函数不存在、或带了无法匹配的 calldata 时触发(代理合约就靠它转发调用)。 两者都必须 payable 才能收款。

合约向外部转钱有三种写法:addr.transfer(amount)(2300 Gas 固定 stipend,失败自动回滚)、addr.send(amount)(不回滚,只返回 bool)、addr.call{value: amount}("")(转发全部可用 Gas,返回 bool 与返回数据)。现代推荐写法是 call + 检查返回值 + 重入保护(bool ok, ) = addr.call{value: amount}(""); require(ok, "transfer failed");

合约怎么收 ETH:receive、fallback 与 call
合约怎么收 ETH:receive、fallback 与 call|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
🚫
避坑提醒:

用 call 向外转钱时,对方 fallback 可以反向调用你的合约——这正是重入攻击的入口,必须配合 nonReentrant 并先更新内部余额。

09

接口、库与继承:组合复用三大工具

登录记录进度

大型合约不靠复制粘贴,而靠三种复用机制: - interface(接口):只声明函数签名、不写实现,用来调用「别人已经部署的合约」。例如知道 Uniswap 路由地址和它的接口,就能在自己的合约里调用它。

ERC-20 的 IERC20 就是接口。 - library(库):无状态的工具集合,不能存状态变量、不能收 ETH、不能被销毁。using SafeMath for uint256(旧版本需要)或 OpenZeppelin 的 SafeERC20 都是库。

  • inheritance(继承)contract MyToken is ERC20, Ownable 表示继承多个父合约,可重写(override)标记为 virtual 的函数。

阅读开源合约时,真正的逻辑常常分散在继承链上:看 ERC20 具体实现要追到 @openzeppelin/contracts/token/ERC20/ERC20.sol。学会「沿着 import 和 is 跳转」比背语法更重要。

接口、库与继承:组合复用三大工具
接口、库与继承:组合复用三大工具|界面示意 · docs.soliditylang.org · 采集于 2026-09-18
💡
小技巧:

入门期最值钱的能力是读 OpenZeppelin 源码:它代码规范、注释完整、安全考量周全,把它的 ERC20/Ownable/Pausable 读懂,胜过看十篇二手教程。

OpenZeppelin Contracts 文档

来源与时效

本章记录平台 docs.soliditylang.org · 客户端 Web / App · 版本 v1 · 最后更新 2026-09-19 Web3 产品的界面与规则更新频繁,动手前请以官方当前界面与公告为准。

这一章可以动手试

智能合约域有 1 个练习, 在浏览器里真跑(不连钱包、不发交易),可以拿它们验证刚读到的结论。

相关百科文章

看完本章后可以延伸阅读这些条目