🧰 เวอร์ชันที่ใช้ตรวจ — Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 · Puma 8.0.2
(สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — เป็นรุ่นแก้บั๊กในสายเดียวกัน เรื่องในหน้านี้ใช้ได้เหมือนกัน)
แอปตัวอย่างทั้งคอร์สคือ บล็อก ชื่อ wongnok_blog · ของหลักคือ บทความ
ภาษาอังกฤษเรียกว่า post · Day 1 ยังไม่มี database จริง — บทความในตัวอย่างหลายตัวจำลองด้วย Hash
(เขียนกำกับไว้ทุกครั้ง)
ของในเครื่องคุณ เช่น เวลา · PID (เลขประจำโปรแกรมที่กำลังรัน) · เลข commit · เลข 0x… จะไม่ตรงกับในหน้านี้ — เป็นเรื่องปกติ
สารบัญ
ภาพร้านอาหาร — Rails ทั้งร้านในตารางเดียว
Post กับ posts — กฎตั้งชื่อ
Ruby เทียบ Go ทีละเรื่อง
map · select · find — ผลรันจริง
5 ไวยากรณ์ที่เจอในโค้ด Rails
rails new · โฟลเดอร์ · Gemfile
คำสั่งที่ใช้บ่อยใน Day 1
routes · resources 8 เส้น · _path
controller · params · render กับ redirect_to
view · ERB · layout · link_to · ความปลอดภัย
debugger · console
ตาราง error → แปลว่า → แก้ยังไง
ศัพท์ภาษาคน
ถ้าเปิดสไลด์ดูประกอบ
อ้างอิง
1 · ภาพร้านอาหาร — Rails ทั้งร้านในตารางเดียว
ทั้งคอร์สใช้ภาพเดียว: แอป Rails คือร้านอาหาร ·
Go ที่เคยเรียน = เปิดร้านเองตั้งแต่ศูนย์ (จ้างพนักงานต้อนรับเองด้วย Gin router · เขียนใบงานเชฟเองด้วย handler · ซื้อตู้เองด้วย GORM) ·
Rails = ร้านแฟรนไชส์ที่ได้ครัวครบชุด ขอแค่วางของตามตำแหน่งที่แฟรนไชส์กำหนด
ส่วนของเว็บ / Rails ในร้านอาหาร อยู่ที่ไหน · หน้าตา ใน Go คือ
browser ลูกค้า Chrome · Safari · หรือคำสั่ง curl ใน terminal client ตัวเดียวกัน
request ใบสั่งอาหาร ใน log: Started GET "/posts" คำขอที่ handler รับผ่าน c *gin.Context
response จานอาหารที่ยกมาเสิร์ฟ HTML + เลขสถานะ เช่น Completed 200 OK c.String(…) · c.Data(…)
server การเปิดร้าน bin/rails server → Puma (โปรแกรม web server ที่มากับ Rails) รอที่พอร์ต 3000r.Run()
middleware ด่านหน้าครัว ที่ใบสั่งทุกใบต้องผ่าน ดูรายชื่อด้วย bin/rails middleware (แอปใหม่มี 28 ด่าน) r.Use(…) · gin.Default() ใส่ให้ 2 ตัว
routes พนักงานต้อนรับ ดูใบสั่งแล้วชี้ว่าแผนกไหนรับ config/routes.rb ไฟล์เดียวทั้งแอปr.GET("/posts", h.List)
controller หัวหน้าเชฟของแผนก (เช่นแผนกบทความ) app/controllers/posts_controller.rbกลุ่ม handler ของเรื่องเดียวกัน
action เมนูแต่ละอย่างของแผนก def index · def show ใน controllerhandler หนึ่งตัว
params รายละเอียดในใบสั่ง (“จานที่ 42”) params[:id] · params[:name]c.Param("id") · c.Query("name")
@ตัวแปร ใน controllerถาดหน้าครัว ที่เชฟวางของให้คนจัดจาน @posts = …gin.H{"posts": posts} ที่ส่งให้ template
model ห้องเก็บวัตถุดิบ + คนดูแลสต็อก app/models/post.rb (ใช้จริง Day 2)struct + GORM
database · ตาราง · แถว ตู้เก็บแฟ้ม · ลิ้นชักบทความ · แฟ้มหนึ่งแผ่น ตาราง posts ใน PostgreSQL ตารางที่ GORM ใช้
view การจัดจานก่อนเสิร์ฟ app/views/posts/index.html.erbแม่แบบ html/template
layout จานรองแบบเดียวกันทุกจาน app/views/layouts/application.html.erbไม่มีให้ — ต้องจัดเอง
helper เครื่องพิมพ์ป้ายบอกทาง link_to · post_path(42)ต่อข้อความเอง fmt.Sprintf("/posts/%d", id)
gem · Gemfile · Gemfile.lock อุปกรณ์สำเร็จรูป · รายการซื้อของ · ใบเสร็จ ไฟล์บนสุดของโปรเจกต์ module · go.mod · go.sum
Convention over Configuration แฟรนไชส์ที่ทุกสาขาวางของที่เดิม รู้คำว่า posts คำเดียว ก็เดาได้ทั้งสาย (หัวข้อ 2) ไม่มี — ต้องบอกทุกอย่างเอง
server log สมุดจดใบสั่งหน้าครัว หน้าต่าง terminal ที่รัน server · log/development.log บรรทัด [GIN] …
bin/rails consoleครัวทดลองหลังร้าน พิมพ์ Ruby ทีละบรรทัด โดยโหลดแอปไว้แล้ว ไม่มี — ต้องเขียน main.go ใหม่
debugger กดหยุดเวลาในครัว แล้วเดินไปเปิดหม้อดู เขียนคำว่า debugger ในบรรทัดที่อยากให้หยุด Delve (dlv) ต้องติดตั้งเอง
migration (Day 2) ใบสั่งช่างมาต่อเติมตู้เก็บแฟ้ม db/migrate/…goose
เส้นทางของใบสั่งหนึ่งใบ (บท 90 · 100) — ผิดตรงไหน ไล่ตามเส้นนี้:
ลูกค้าพิมพ์ localhost:3000/posts
│ ใบสั่ง: GET /posts
▼
ประตูร้าน (Puma) ──▶ ด่านหน้าครัว (middleware) ── จดสมุด: Started GET "/posts"
▼
พนักงานต้อนรับ (config/routes.rb) "GET /posts → แผนก posts เมนู index"
▼
หัวหน้าเชฟ (PostsController#index) วางของบนถาด: @posts = [...]
│ ╲ (Day 2) ห้องเก็บวัตถุดิบ = model Post + database
▼
จัดจาน (app/views/posts/index.html.erb) วางบนจานรอง (layout)
▼
ยกจานเสิร์ฟ = response: HTML + 200 OK
🍜 ทำไมใช้ภาพร้านอาหาร
แต่ละคนในร้านทำงานอย่างเดียวแล้วส่งต่อ — พนักงานต้อนรับไม่ทำอาหาร · หัวหน้าเชฟไม่ค้นตู้เอง · คนดูแลสต็อกไม่จัดจาน ·
หน้าเว็บผิดจึงรู้ทันทีว่าต้องไปหาใคร: URL ไปผิดทาง → routes · ข้อมูลผิด → model · หน้าตาผิด → view
ที่ไม่เหมือนกัน: ในร้านจริงมีคนเดินไปมา · ใน Rails ทุก “คน” คือโค้ดในโปรแกรมเดียวที่เรียกต่อกัน
เสร็จในไม่กี่มิลลิวินาที · และหัวหน้าเชฟ (controller) เป็นคนใหม่ทุกใบสั่ง — จำอะไรจากใบสั่งก่อนไม่ได้เลย
2 · Post กับ posts — กฎตั้งชื่อ
กฎจำง่าย: ตัวใหญ่เอกพจน์ (Post) = แม่พิมพ์ของบทความหนึ่งชิ้น ·
ตัวเล็กพหูพจน์ (posts) = ที่เก็บหลายชิ้น หรือ URL · ชื่อเดียวแตกไปอยู่ได้ทุกที่ข้างล่าง —
Rails จับคู่ให้เองจากชื่อ ผิดตัวเดียว (เช่นขาด s) ก็หาไม่เจอ
ของ ชื่อ หนึ่ง / หลายชิ้น ภาษาคน ใน Go คือ
model (class) Postหนึ่ง แม่พิมพ์: บทความหนึ่งชิ้นมีอะไรบ้าง type Post struct
ไฟล์ model app/models/post.rbหนึ่ง ชื่อไฟล์ = ชื่อ class ตัวเล็ก ไฟล์ .go ตั้งชื่ออะไรก็ได้
ตารางใน database postsหลาย ลิ้นชักที่เก็บบทความทุกชิ้น ตาราง posts ที่ GORM เดาให้
URL /posts · /posts/42หลาย หน้ารวม · บทความเบอร์ 42 ในกลุ่มบทความ r.GET("/posts/:id", …)
controller (class) PostsControllerหลาย แผนกที่ดูแลบทความทั้งกลุ่ม กลุ่ม handler
ไฟล์ controller app/controllers/posts_controller.rbหลาย ตัวเล็ก คั่นคำด้วย _ —
โฟลเดอร์ view app/views/posts/หลาย เก็บหน้าตาทุกหน้าของแผนกบทความ —
ไฟล์ view ของเมนู index app/views/posts/index.html.erb— ชื่อแผนก + ชื่อเมนู = ที่อยู่ไฟล์ ต้องบอกชื่อ template เองทุกครั้ง
ชื่อใน route "posts#index"หลาย ซ้าย # = แผนก · ขวา = เมนู ส่งตัวฟังก์ชัน h.List ตรง ๆ
สองรูปแบบการเขียน: ชื่อ class ตัวใหญ่ต้นคำเขียนติดกัน (PostsController · เรียก CamelCase ) ·
ชื่อไฟล์และตารางตัวเล็กหมดคั่นด้วย _ (posts_controller.rb · เรียก snake_case ) ·
class BlogPost ต้องอยู่ไฟล์ blog_post.rb
ให้ Rails แปลงชื่อให้ดู — พิมพ์ใน bin/rails console (ผลจริง บท 99)
พิมพ์ ได้ ภาษาคน
"Post".tableize"posts"ชื่อ model → ชื่อตาราง
"posts".classify"Post"ชื่อตาราง → ชื่อ class
"post".pluralize"posts"ทำเป็นพหูพจน์
"people".singularize"person"ทำเป็นเอกพจน์
"Category".tableize"categories"y → ies
"Person".tableize · "Mouse".tableize · "Sheep".tableize"people" · "mice" · "sheep"รู้จักคำพิเศษ ไม่ได้แค่เติม s
"human".pluralize"humen" ⚠️ผิด! Rails มีแค่กฎตัดท้ายคำ · ชื่อไม่คุ้นให้ลองที่นี่ก่อนสร้าง model
❓ “ใน Go ต้องประกาศ struct ชัด ๆ แล้ว Post โผล่มาเฉย ๆ ได้ไง?” — คำตอบมีสองครึ่ง (บท 90 · 95 · 99)
มีไฟล์ประกาศจริง คือ app/models/post.rb (สร้างด้วย bin/rails generate model Post title:string body:text) · ทั้งไฟล์มีแค่
class Post < ApplicationRecord / end · และไม่ต้อง import — พอโค้ดเรียก Post Rails แปลงเป็นชื่อไฟล์ post.rb แล้วไปเปิดให้เอง (เรียกว่า autoload )
ช่องข้อมูลมาจากตาราง ไม่ได้มาจากไฟล์ · ตอนรัน Rails เปิดตาราง posts ดูว่ามีคอลัมน์อะไร แล้วสร้าง method ให้ ·
bin/rails runner 'p Post.column_names' ได้ ["id", "title", "body", "created_at", "updated_at"] ·
Go กลับกัน: เขียน struct ก่อน แล้ว GORM ทำตารางตาม
3 · Ruby เทียบ Go ทีละเรื่อง
อ่านแถวละเรื่อง · คอลัมน์ขวาสุดคือสิ่งที่ต่างจาก Go จนพลาดบ่อย · ตัวเลขในวงเล็บคือบทที่สอน
พื้นฐาน — ตัวแปร · string · object (91)
เรื่อง Go Ruby ภาษาคน / จุดที่ต่าง
comment // …# …Ruby ข้ามไม่รัน · # ใน "#{}" ไม่ใช่ comment
ลองโค้ดทีละบรรทัด ต้องมี package main + func main() พิมพ์ irb ใน terminal กระดาษทดเลข · พิมพ์ exit แล้วหายหมด
ตัวแปร title := "Hi"title = "Hi"ไม่ต้องบอกชนิด · พิมพ์ชื่อผิดตอนให้ค่า Ruby คิดว่าเป็นตัวใหม่ ไม่เตือน
ค่าคงที่ const BlogName = "x"BLOG_NAME = "x"แค่ขึ้นต้นตัวใหญ่ · เปลี่ยนได้แต่โดน warning: already initialized constant
ตัวใหญ่ต้นชื่อแปลว่า export ให้ package อื่นใช้ เป็นค่าคงที่ (ชื่อ class ก็ด้วย) ความหมายคนละเรื่องกัน
แทรกค่าในข้อความ fmt.Sprintf("บทความ: %s", t)"บทความ: #{t}"ต้องใช้ "…" คู่ · '…' เดี่ยวพิมพ์ #{t} ออกมาตรงตัว
ข้อความ + ตัวเลข "อ่าน " + views → build ไม่ผ่าน"อ่าน " + views → TypeError ตอนรันใช้ "อ่าน #{views}" (แปลงให้เอง) หรือ views.to_s
สั่งงานค่า len(t) · strings.ToUpper(t)t.length · t.upcase · 42.even?ทุกค่าเป็น object มี method ติดตัว เรียกด้วยจุด
ต่อหลายขั้น len(strings.ToUpper(strings.TrimSpace(s))) อ่านในออกนอกs.strip.upcase.length อ่านซ้ายไปขวาเรียกว่า method chaining · เจอใน Rails บ่อยมาก
ถามชนิด รู้จาก type ที่ประกาศ 42.class → Integer · nil.class → NilClassถามได้ทุกค่า เพราะทุกค่าเป็น object
ไม่มีค่า nil (pointer · slice)nilRuby เจอบ่อยกว่ามาก: หาช่องใน Hash ไม่เจอ · ขอช่อง Array ที่ไม่มี · method ที่ไม่ได้คืนอะไร
ของหลายชิ้น — Array · Hash · Symbol (92)
เรื่อง Go Ruby ภาษาคน / จุดที่ต่าง
รายการ []int{10, 20, 30}[10, 20, 30]Array = slice · ช่องแรกคือ 0
เพิ่มท้าย s = append(s, 40)s << 40<< อ่านว่า “ยัดต่อท้าย” (shovel)
ความยาว len(s)s.lengthถามจาก Array เองด้วยจุด
ชิ้นสุดท้าย ไม่มีเลขติดลบให้ใช้ s[-1]เลขติดลบ = นับจากท้าย
ขอช่องที่ไม่มี panic: runtime error: index out of ranges[10] → nil เงียบ ๆเห็น nil งง ๆ ให้สงสัยเลขช่องก่อน
คู่ชื่อ–ค่า map[string]int{"a": 1}{ a: 1 }Hash = map · ในตัวอย่าง Day 1 ใช้จำลองบทความหนึ่งชิ้น
หาไม่เจอ v, ok := m["b"] → 0 falsem[:b] → nil · m.fetch(:b, 0) → 0Ruby ไม่มี ok · อยากให้พังชัด ๆ ใช้ fetch(:b) → KeyError
ป้ายชื่อ ใกล้สุดคือ const StatusActive = "active" :title (symbol)ชื่อเรียก ไม่ใช่ข้อความโชว์ผู้ใช้ · title: ตอนสร้าง กับ [:title] ตอนหยิบ = ตัวเดียวกัน
key คนละชนิด key เป็น string เสมอ post[:title] ≠ post["title"]Hash ที่เก็บด้วย :title หยิบด้วย "title" ได้ nil · ยกเว้น params ของ Rails ที่หยิบได้ทั้งสองแบบ
method และ block (93)
เรื่อง Go Ruby ภาษาคน / จุดที่ต่าง
ประกาศ func Greet(name string) string { … }def greet(name) … endไม่บอกชนิด · ปิดด้วย end ไม่ใช่ปีกกา · ชื่อใช้ snake_case
ส่งค่ากลับ return x บังคับบรรทัดสุดท้ายที่รัน = ค่าที่ส่งกลับ เผลอแปะ puts ไว้ท้าย method → ได้ nil (puts คืน nil เสมอ)
method บรรทัดเดียว — def add(a, b) = a + bมี = ไม่มี end
ส่งของโดยบอกชื่อช่อง ส่ง struct · ลืม field ได้ค่าศูนย์เงียบ ๆ def make_post(title:, draft: false)title: ไม่มีค่าตาม = บังคับ · ลืมส่ง → missing keyword: :title
ถามใช่/ไม่ใช่ func IsDraft(p Post) booldraft?(post) · [].empty?? เป็นตัวอักษรในชื่อ · ข้อตกลง = ตอบ true/false
แก้ของเดิม — name.upcase!! = “อันตรายกว่า” · ไม่มีอะไรต้องเปลี่ยน → คืน nil อย่าเอาไปต่อ
ส่งก้อนโค้ด ส่ง func(n int) { … } เป็น argument { |n| … } หรือ do |n| … endเรียกว่า block · |n| = ชื่อของชิ้นที่ไหลมาถึง · บรรทัดเดียวใช้ { } หลายบรรทัดใช้ do … end
วนทีละชิ้น for _, n := range nums { … }nums.each { |n| puts n }คน Ruby แทบไม่เขียน for
ทำซ้ำ N ครั้ง for i := 0; i < 3; i++ { … }3.times { |i| … }นับจาก 0 · ตัวเลขก็มี method
เรียกก้อนที่ฝากมา เรียก f(n) yieldไม่มีใครฝาก block มา → no block given (yield)
พิมพ์ดูค่า fmt.Printlnputs (ให้คนอ่าน · คืน nil) · p (ให้โปรแกรมเมอร์ · คืนของเดิม)debug ใช้ p — เห็น "1" ต่างจาก 1 และเห็นคำว่า nil
จริง/เท็จ และ nil (94)
อยากรู้ว่า… Go Ruby อย่าเขียน / กับดัก
กฎของ if ต้องเป็น bool · if views → non-boolean condition ไม่ผ่านแค่ nil กับ false 0 · "" · " " · [] · "false" ผ่านหมด
ตัวเลขมากกว่า 0 ไหม if views > 0views.positive? · เท่ากับ 0: views.zero?if views — 0 ก็ผ่าน
เป็น nil ไหม x != nilx.nil?if !x แยก nil กับ false ไม่ออก
เรียก method ถ้ามีค่า if p.Author != nil { … }post[:author]&.upcaseไม่มีค่าได้ nil กลับมา — เลื่อนปัญหาไปข้างหน้า ไม่ได้แก้
ใส่ค่าเริ่มต้นถ้ายังว่าง if tags == nil { tags = … }tags ||= ["general"]ค่า false ที่ตั้งใจใส่ โดนทับเป็นค่าใหม่ · ใช้ x = true if x.nil? แทน
ช่องฟอร์มที่ผู้ใช้ปล่อยว่าง strings.TrimSpace(q) == ""q.blank? · q.presence || "all"q || "all" — "" กับ " " ผ่าน || ไปเลย
Active Support (ของที่ Rails เติมให้ Ruby) ผลจริง ภาษาคน
"".blank? · " ".blank? · nil.blank? · [].blank? · false.blank?true ทั้งหมด“ว่าง” แบบที่คนทั่วไปมอง
0.blank?falseตัวเลขไม่เคยว่าง
"ramen".present?trueตรงข้ามกับ blank? เป๊ะ
"ramen".presence · " ".presence · nil.presence"ramen" · nil · nilมีของจริงส่งตัวมันมา · ว่างส่ง nil — ใช้คู่กับ ||
3.days.agoวันเวลาเมื่อ 3 วันก่อน (ใน console ของแอปใหม่แสดงเป็นโซน UTC) Ruby เปล่าพัง NoMethodError · ในแอป Rails ใช้ได้เลย
นอกแอป Rails ต้องสั่งโหลด Active Support เอง: ruby -ractive_support/all ไฟล์.rb (-r = โหลดสิ่งนี้ก่อนรัน)
class และ module (95)
เรื่อง Go Ruby ภาษาคน / จุดที่ต่าง
แม่พิมพ์ type Post struct { Title string }class Post … endแม่พิมพ์ขนมไข่ · ของแต่ละชิ้นที่ได้คือ object
สร้างของหนึ่งชิ้น NewPost("Hi") · Post{Title: "Hi"}Post.new("Hi")new ไปเรียก def initialize(title) ให้เอง
ช่องข้อมูล field Title @title = title@ = ติดอยู่กับ object ตัวนี้ ไม่หายตอน method จบ
อ่านจากข้างนอก post.Title ได้เลยถ้าตัวใหญ่ต้องมี method · attr_accessor :title (อ่าน+แก้) · attr_reader :title (อ่านอย่างเดียว) ไม่มี → undefined method 'title' for an instance of Post
method ของแม่พิมพ์ func (p Post) Greeting() stringdef greeting ข้างใน classไม่ต้องเขียน receiver · @title ≈ p.Title
สืบทอด ไม่มี (ใช้ฝัง struct) class Recipe < Post · super(title)< ตรงนี้ไม่ใช่น้อยกว่า · super ไม่ใส่วงเล็บ = ส่ง argument ทุกตัวต่อ
แชร์ method ข้าม class ฝัง struct (embedding) module Sluggable + include Sluggableกล่องเครื่องมือกลาง · module สร้าง object เองไม่ได้ · ใน Rails เก็บที่ concerns/
@ตัวแปร สะกดผิดfield ผิด → compile ไม่ผ่าน @titel → nil เงียบ ๆผลว่างแปลก ๆ ให้สงสัยตัวสะกดของ @… ก่อน
error และ duck typing (96)
เรื่อง Go Ruby ภาษาคน / จุดที่ต่าง
error คืออะไร ค่าที่ return มาคู่ผลลัพธ์ (int, error) exception ที่ถูก “โยน” ขึ้นไปจนกว่าจะมีคนรับ กล่องจดหมาย vs กริ่งไฟไหม้ · ไม่มีใครรับ = โปรแกรมหยุด
รับ error if err != nil { … } ทุกจุดbegin … rescue … endครอบทีเดียวได้หลายบรรทัด · ≈ try/catch ของ TS
รับเฉพาะชนิด เช็คค่า err เอง rescue ZeroDivisionError · rescue ArgumentError => eเข้าช่องแรกที่ชนิดตรง · => e เก็บ error ไว้ดู e.message
สร้าง error เอง errors.New("…")class OutOfStockError < StandardError; end · raise OutOfStockError, "…"ต้อง < StandardError · ถ้า < Exception แล้ว rescue => e ไม่รับ
ทำเสมอ deferensureป้าย “ออกจากครัวต้องปิดแก๊ส” · พังหรือไม่ก็ทำ
rescue ทั้ง method — เขียน rescue ก่อน end ของ def ไม่ต้องมี begin Rails ใช้ท่านี้บ่อย
รับ “อะไรก็ได้ที่มี title” ประกาศ interface · compiler ตรวจตอน build ไม่ประกาศอะไร เรียก x.title เลย duck typing · ส่งผิดพังตอนรัน (NoMethodError) · ถามก่อนได้ด้วย respond_to?(:title)
ไม่มีใครรับใน Rails — Rails รับชั้นบนสุดให้ ตอบ 500 พังแค่ใบสั่งนั้น ร้านยังเปิด · รายละเอียดอยู่ใน server log
4 · map · select · find — ผลรันจริง (บท 92)
สายพานในครัว: ของไหลมาทีละชิ้นผ่านสถานีต่าง ๆ · ทุกตัวไม่แตะ Array เดิม สร้างของใหม่ให้ ·
ตัวอย่างใช้ nums = [3, 1, 4, 1, 5]
โค้ด ภาษาคน ผลจริง ใน Go คือ
nums.map { |n| n * 2 }ทำอย่างเดียวกันกับทุกชิ้น [6, 2, 8, 2, 10]for + append ลงถังใหม่
nums.select { |n| n > 2 }เก็บเฉพาะชิ้นที่ผ่าน (= filter ของ TS) [3, 4, 5]for + if + append
nums.reject { |n| n > 2 }ทิ้งชิ้นที่ผ่าน [1, 1]เหมือนข้างบน กลับเงื่อนไข
nums.find { |n| n > 2 }ชิ้นแรกที่ใช่ แล้วหยุด 3for + if + break
nums.find { |n| n > 100 }หาไม่เจอ nil—
nums.select { |n| n > 100 }คัดแล้วไม่เหลือ [] (ไม่ใช่ nil)—
nums.sum · nums.sortรวม · เรียงน้อยไปมาก 14 · [1, 1, 3, 4, 5]วน for เอง
nums.min · nums.maxน้อยสุด · มากสุด 1 · 5วนเทียบเอง
p nums หลังทำทั้งหมดของเดิมยังอยู่ครบ [3, 1, 4, 1, 5]—
กับบทความหลายชิ้น (จำลองด้วย Array ของ Hash · 3 ชิ้น ยอดอ่าน 120 · 40 · 300):
โค้ด ภาษาคน ผลจริง
posts.select { |post| post[:published] }.map { |post| post[:title] }เลือกที่เผยแพร่แล้ว → เอาแต่ชื่อ ["ต้มยำกุ้ง", "เที่ยวน่าน"]
posts.sum { |post| post[:views] }รวมยอดอ่าน 460
posts.sort_by { |post| post[:views] }.map { |post| post[:title] }เรียงตามยอดอ่าน แล้วดูแค่ชื่อ ["ส้มตำ", "ต้มยำกุ้ง", "เที่ยวน่าน"]
posts.find { |post| post[:views] > 100 }บทความทั้งก้อนชิ้นแรกที่เกิน 100 {title: "ต้มยำกุ้ง", published: true, views: 120}
⚠ ทางลัด &:ชื่อ ใช้กับ Hash ไม่ได้
names.map(&:upcase) = names.map { |name| name.upcase } (“ทุกชิ้น เรียก method ชื่อนี้”) ·
แต่ posts.map(&:title) กับ Array ของ Hash พัง undefined method 'title' for an instance of Hash —
Hash มีช่อง :title ไม่ได้มี method title · กับ Hash ใช้ { |post| post[:title] } ·
&:title ใช้ได้กับ Post ตัวจริงใน Rails (Day 2)
5 · 5 ไวยากรณ์ที่เจอในโค้ด Rails (บท 97)
โค้ด Rails ที่อ่านเหมือนภาษาอังกฤษ ไม่มีเวทมนตร์ — ทุกบรรทัดคือการเรียก method ธรรมดาที่เขียนย่อ ·
อ่านแล้วงงเมื่อไหร่ ให้นึกภาพ “แบบเขียนเต็ม” ในคอลัมน์กลาง
ชิ้น เห็นในโค้ด Rails แบบเขียนเต็ม (ไว้อ่าน) ภาษาคน ใน Go
1 · วงเล็บละได้ greet "ต้น"greet("ต้น")เรียก method ตัวเดียวกัน ต้องมีวงเล็บเสมอ
2 · hash ตัวสุดท้ายละปีกกาได้ validates :title, presence: truevalidates(:title, { presence: true })เห็น ชื่อ: ค่า ท้ายบรรทัด = hash ก้อนเดียวที่ถอดปีกกา ต้องสร้าง struct แล้วใส่ปีกกาเอง
2 · (อีกตัวอย่าง) redirect_to @post, notice: "saved"redirect_to(@post, { notice: "saved" })ชิ้นแรกคือ @post · ที่เหลือรวมเป็น hash —
3 · block ที่ได้รับของ form_with model: @post do |f|form_with(model: @post) do |f| — ห้ามใส่ปีกกา form_with สร้างกล่องเครื่องมือ แล้ว yield ยื่นให้ block → กลายเป็น fc ใน func(c *gin.Context) ที่ Gin ยื่นให้
4 · symbol ทุกที่ f.text_field :title · status: :see_otherf.text_field(:title)โคลอนหน้า (:title) หรือหลัง (presence:) ก็เป็น symbol ทั้งคู่ ใช้ string เป็นชื่อช่อง gin.H{"title": …}
5 · method ที่ Rails สร้างตอนรัน post.title ทั้งที่ post.rb ว่าง(เหมือนเดิม) Rails อ่านคอลัมน์ในตาราง แล้วสร้าง title · title= ให้ · ดูว่ามีช่องอะไรที่ db/schema.rb เขียน field ทุกตัวใน struct เอง
⚠ เขียนเต็มไว้ “อ่าน” เท่านั้น อย่าเติมปีกกาเองตอนเรียก form_with
form_with รับของแบบ keyword (ส่งด้วยชื่อ) · ใส่ปีกกาแล้วกลายเป็น hash หนึ่งก้อน ได้ error จริง:
wrong number of arguments (given 1, expected 0; required keyword: model) (ArgumentError) ·
เขียนแบบไม่มีปีกกา (แบบในคู่มือ Rails) ใช้ได้กับทุกตัว
ท่า “method ยื่นของให้ block” ที่จะเจออีก: <%= yield %> ใน layout (บท 103) ·
create_table :posts do |t| ใน migration (Day 2) · respond_to do |format| ใน controller (Day 3) ·
เจอ |x| ที่ไม่รู้จัก ให้ถามว่า “method ไหนยื่นของชิ้นนี้มา”
6 · rails new · โฟลเดอร์ · Gemfile (บท 98)
ท่อนของ rails new wongnok_blog --database=postgresql --css=tailwind ภาษาคน ในร้านอาหาร
rails newสร้างโปรเจกต์ใหม่ (ได้ 109 ไฟล์ · เปิดเป็นเว็บได้ทันที) ขอเปิดสาขาใหม่จากบริษัทแม่
wongnok_blogชื่อโปรเจกต์ = ชื่อโฟลเดอร์ ชื่อสาขา
--database=postgresqlใช้ PostgreSQL (ไม่ใส่ = SQLite ซึ่งเป็น database ไฟล์เดียว) ขอตู้เก็บแฟ้มยี่ห้อนี้
--css=tailwindติดตั้ง Tailwind ไว้แต่งหน้าเว็บ ขอชุดตกแต่งร้าน
Go — เช่าห้องเปล่า
$ go mod init blog
go: creating new go.mod: module blog
$ ls -A
go.mod
Rails — ได้ร้านครบ (แต่ยังไม่ commit)
$ git log
fatal: your current branch 'main' does not have any commits yet
$ git add -A
$ git commit -m "rails new"
rails new แค่ git init ให้ ไม่ได้ commit ให้ — commit แรกเองทันที จะได้เทียบได้ว่าแก้อะไรไป
ห้องในร้าน — Day 1 แตะแค่ไม่กี่ห้อง
โฟลเดอร์ / ไฟล์ มีไว้ทำอะไร ในร้านอาหาร ใน Go ที่เคยทำ Day 1
app/controllers/หัวหน้าเชฟแต่ละแผนก ห้องเชฟ handler ✅ บ่อย
app/views/ไฟล์ HTML ที่ส่งให้ลูกค้า โต๊ะจัดจาน (ฝั่ง Go ส่ง JSON ด้วย c.JSON) ✅ บ่อย
app/models/ดูแลข้อมูลในตู้แฟ้ม ห้องวัตถุดิบ struct + GORM ❌ รอ Day 2
config/routes.rbรายการ URL ทั้งแอป รายการในมือพนักงานต้อนรับ r.GET(…) ใน main.go✅ บ่อย
bin/สคริปต์คำสั่ง เช่น bin/rails แผงสวิตช์ของร้าน go run .✅ ใช้ทุกวัน ไม่ต้องแก้
Gemfile · Gemfile.lockgem ที่ใช้ · รุ่นจริงที่ได้มา รายการซื้อของ · ใบเสร็จ go.mod · go.sum👀 เปิดอ่านบ้าง
log/บันทึกทุก request สมุดจดใบสั่ง log ของ Gin 👀 ดูตอนพัง
db/เรื่องของ database (db/migrate/ · db/schema.rb เกิดตอนสร้างตารางแรก) ตู้เก็บแฟ้ม ไฟล์ migration ของ goose ❌ รอ Day 2
test/โค้ดทดสอบ ห้องชิมก่อนเสิร์ฟ _test.go❌ รอ Day 4
public/ไฟล์ที่ส่งตรงไม่ผ่านครัว (เช่นหน้า 404) ใบปลิวหน้าร้าน — ❌
ที่เหลือ (lib/ tmp/ storage/ vendor/ …) ของเครื่องมือ · deploy · ไฟล์ชั่วคราว ห้องเก็บของหลังร้าน — ❌ ไม่ต้องสนใจ
Gemfile เทียบ go.mod
บรรทัดใน Gemfile ภาษาคน ใน Go คือ
source "https://rubygems.org"ไปซื้อของที่คลังไหน แหล่งที่ go get โหลด module
gem "rails", "~> 8.1.4"เรียก method gem แบบละวงเล็บ: ชื่อ gem + เงื่อนไขรุ่น require github.com/… v1.12.0
gem "pg", "~> 1.1"ตัวคุยกับ PostgreSQL driver postgres ที่ GORM ใช้
gem "puma", ">= 5.0"web server (ประตูร้าน) · รุ่น 5.0 ขึ้นไป net/http ที่ Gin ใช้
group :development, :test do … endใช้เฉพาะเครื่องเรากับตอนเทส ไม่โหลดบนเว็บจริง —
gem "brakeman", require: falseติดตั้งไว้ แต่ไม่โหลดเข้าแอปเอง (เครื่องมือเรียกจาก terminal) —
เงื่อนไขรุ่น ~> (“ขยับได้ แต่ไม่ไกล”) ผ่าน ไม่ผ่าน
~> 1.2 = ตั้งแต่ 1.2 แต่ต่ำกว่า 2.01.2.0 · 1.14.0 · 1.99.91.1.0 · 2.0.0
~> 8.1.4 = ตั้งแต่ 8.1.4 แต่ต่ำกว่า 8.28.1.4 · 8.1.98.1.3 · 8.2.0
Gemfile.lock = ใบเสร็จ: รุ่นจริง + ลายนิ้วมือ (checksum) ของทุก gem · เขียนในรายการ 24 ตัว ได้มาจริง 124 ตัว (gem ลากเพื่อนมาด้วย) · commit เสมอ ห้ามแก้ด้วยมือ (หน้าที่เดียวกับ go.sum)
bundle install = ให้ Bundler (คนถือรายการไปซื้อของ) ติดตั้งตาม Gemfile.lock · clone โปรเจกต์ใครมา สั่งตัวนี้ก่อน ≈ go mod download
บอก Rails ว่า PostgreSQL อยู่ไหน (ก่อนเปิด server · หน้าต่าง terminal เดียวกัน): export DATABASE_URL=postgres://ผู้ใช้:รหัส@127.0.0.1:พอร์ต/ชื่อdb แล้ว bin/rails db:prepare → Created database '…' · หน้าตาเดียวกับ DSN (ที่อยู่ database แบบบรรทัดเดียว) ที่ใช้กับ goose
7 · คำสั่งที่ใช้บ่อยใน Day 1
ในโฟลเดอร์โปรเจกต์ใช้ bin/rails เสมอ (ใช้ gem รุ่นตาม Gemfile.lock ของโปรเจกต์นี้) · rails เฉย ๆ ใช้แค่ตอน rails new
อยากทำอะไร พิมพ์ ควรเห็น / ต้องรู้ ใน Go คือ
เช็ครุ่น Ruby ruby -vขึ้นต้น ruby 4. · ถ้าได้ ruby 2.6.10… = Ruby รุ่นเก่าที่ติดมากับ Mac (terminal ยังไม่ได้ปลุก mise — ตัวสลับรุ่น Ruby ที่ติดตั้งในบท 91) go version
รันไฟล์ Ruby ruby ไฟล์.rbอ่านไปรันไปทีละบรรทัด · บรรทัดก่อนจุดพังทำงานไปแล้ว go run main.go
ตรวจไวยากรณ์อย่างเดียว ruby -c ไฟล์.rbผ่านได้ Syntax OK · ใช้หา end ที่หาย go build (ส่วนตรวจ)
ลองโค้ดทีละบรรทัด irbprompt irb(main):001> · ผลขึ้นหลัง => ไม่มี
สร้างโปรเจกต์ rails new wongnok_blog --database=postgresql --css=tailwindแล้ว commit แรกเอง go mod init
ติดตั้ง gem ของโปรเจกต์ bundle installBundle complete! …go mod download
สร้าง database bin/rails db:prepareCreated database '…' · ต้องตั้ง DATABASE_URL ก่อน—
เปิดร้าน bin/rails server (ย่อ s)Listening on http://127.0.0.1:3000 · ปิดด้วย Ctrl-C · แก้ routes/controller/view แล้ว refresh ได้เลยr.Run()
ครัวทดลอง bin/rails console (ย่อ c)prompt wongnok-blog(dev):001> · ออกด้วย exit ไม่มี
ครัวทดลองแบบลองแล้วทิ้ง bin/rails console --sandboxAny modifications you make will be rolled back on exitdefer tx.Rollback()
รันไฟล์ Ruby ในแอป bin/rails runner ไฟล์.rb · bin/rails runner '…'รู้จัก Post โดยไม่ต้อง import —
สร้าง model (สาธิตบท 99) bin/rails generate model Post title:string body:text (ย่อ g)ได้ไฟล์ migration (ใบสั่งสร้างตาราง) · app/models/post.rb · ไฟล์เทส test/models/post_test.rb · ข้อมูลตัวอย่างไว้เทส test/fixtures/posts.yml เขียน struct เอง
สร้างตารางตาม migration bin/rails db:migrate== … CreatePosts: migratedgoose (บท 31)
ย้อน migration ล่าสุด bin/rails db:rollbackต้อง rollback ก่อน destroy —
ลบไฟล์ที่ generate ไว้ bin/rails destroy model Categoryบรรทัด remove … —
ดู URL ของแผนกหนึ่ง bin/rails routes -c postsตาราง Prefix · Verb · URI Pattern · Controller#Action อ่าน main.go เอง
ดูด่านหน้าครัว bin/rails middlewareบรรทัด use … เรียงตามลำดับที่ใบสั่งผ่าน r.Use(…) ที่เขียนไว้
แอปนี้ใช้อะไรรุ่นไหน bin/rails aboutRails · Ruby · Database adapter · schema version —
มีคำสั่งอะไรบ้าง bin/rails --help · bin/rails generate model --helpสามตัวบนสุด: generate · console · server go help
ขอหน้าเว็บจาก terminal curl -s http://localhost:3000/postsได้ HTML เป็นตัวหนังสือ · -i ดู header · -L เดินตาม redirect · -w '%{http_code}\n' ดูเลขสถานะ เหมือนกัน
หา debugger ที่ลืมไว้ grep -rn debugger app/ไม่ขึ้นอะไรเลย = สะอาด พร้อม commit —
8 · routes · resources 8 เส้น · _path (บท 101)
routes คือพนักงานต้อนรับ — ดู verb (อยากทำอะไร: GET = ขอดู · POST = ส่งของใหม่) กับ
path (เรื่องอะไร) แล้วชี้ว่าแผนกไหน เมนูอะไร · ไม่ทำอาหาร ไม่แตะ database
Go · Gin
r.GET("/posts" , h.List)
r.GET("/posts/:id" , h.Get)
Rails · config/routes.rb
Rails.application.routes.draw do
get "/posts" , to: "posts#index"
get "/posts/:id" , to: "posts#show" , as: :post
root "posts#index"
end
ชิ้น ภาษาคน ใน Gin
get "/posts", to: "posts#index"ใบสั่ง GET /posts → แผนก posts (PostsController) เมนู index · get คือ method ที่ละวงเล็บ · to: คือ hash ไม่มีปีกกา r.GET("/posts", h.List)
"posts#index"ส่งแค่ชื่อเป็นข้อความ · # ในเครื่องหมายคำพูดเป็นตัวคั่น ไม่ใช่ comment ส่งตัวฟังก์ชัน · พิมพ์ผิด build ไม่ผ่าน
:id ใน pathช่องว่างในใบสั่ง รับอะไรก็ได้ → params[:id] · เป็น String เสมอ ("42") · /posts/abc ก็เข้า c.Param("id") ได้ string
root "posts#index"หน้าแรกของร้าน (path /) · มีได้ตัวเดียว r.GET("/", …)
as: :postตั้งชื่อเล่นให้ route → ได้ helper post_path · path ที่มี : ต้องตั้งเอง ไม่มี
post_path(42) · posts_path · root_path"/posts/42" · "/posts" · "/" · เปลี่ยน URL ในไฟล์เดียว ลิงก์เปลี่ยนตามทุกที่fmt.Sprintf("/posts/%d", id)
post_url(42)URL เต็มพร้อมโดเมน http://localhost:3000/posts/42 (ใช้ในอีเมล) · ลิงก์ในเว็บใช้ _path พอ —
namespace :admin do … endโซนหลังร้าน: URL /admin/posts · แผนก Admin::PostsController · helper admin_posts_path · ไม่ได้ล็อกอะไร r.Group("/admin") (ใกล้ scope)
resources :posts — ชุดเมนูมาตรฐาน 8 เส้นในบรรทัดเดียว
ใบสั่ง เมนู (action) ภาษาคน helper
GET /postsindexดูบทความทั้งหมด posts_path
GET /posts/42showดูบทความชิ้นเดียว post_path(42)
GET /posts/newnewเปิดฟอร์มเขียนใหม่ new_post_path
POST /postscreateกดบันทึกบทความใหม่ (ใช้ posts_path)
GET /posts/42/editeditเปิดฟอร์มแก้ edit_post_path(42)
PATCH /posts/42updateกดบันทึกที่แก้ (ใช้ post_path(42))
PUT /posts/42updateบันทึกที่แก้ อีกวิธีเรียก (เมนูเดียวกับ PATCH) (ใช้ post_path(42))
DELETE /posts/42destroyลบบทความ (ใช้ post_path(42))
resources ให้แค่รายการของพนักงานต้อนรับ — เมนูใน controller ยังต้องเขียน def index · def show … เอง ·
เลือกบางเส้นได้: resources :posts, only: [:index, :show] · ใช้ครบ 8 เส้น (CRUD = สร้าง · อ่าน · แก้ · ลบ) ใน Day 2 · ใน Gin ต้องเขียน r.GET r.POST r.PATCH r.DELETE เองทีละบรรทัด
⚠ ลำดับบรรทัดสำคัญ — บรรทัดแรกที่เข้าเงื่อนไขชนะ (ต่างจาก Gin)
get "/posts/:id" , to: "posts#show" # ← :id รับทุกอย่าง รวมคำว่า popular
get "/posts/popular" , to: "posts#popular" # ← ไม่มีวันถูกเรียก
# $ curl localhost:3000/posts/popular
แผนก posts · เมนู show · params[:id] = "popular"
กฎ: เจาะจงไว้บน กว้างไว้ล่าง · ไม่มีอะไรเตือน · Gin เลือกตัวที่เจาะจงกว่าให้เอง แต่ Rails ไม่ ·
(/posts กับ /posts/:id สลับกันได้ เพราะไม่แย่ง URL เดียวกัน)
9 · controller · params · render กับ redirect_to (บท 100 · 102)
Go · Gin — handler ตัวเดียวทำทุกอย่าง
r.GET("/hello" , func (c *gin.Context) {
name := c.Query("name" )
if name == "" {
name = "PEA"
}
c.String(http.StatusOK, "Hello %s" , name)
})
Rails — 3 ไฟล์ ตามทางที่ใบสั่งเดิน
# config/routes.rb
get "/hello" , to: "greetings#index"
# app/controllers/greetings_controller.rb
class GreetingsController < ApplicationController
def index
@name = params[:name ].presence || "PEA"
end
end
# app/views/greetings/index.html.erb
<h1>Hello <%= @name %></h1>
เรื่อง ภาษาคน ใน Gin
class GreetingsController < ApplicationControllerแผนก greetings · สืบทอดจากแม่แบบที่ rails new สร้างไว้ จึงได้ params · render · redirect_to มาฟรี · เขียน < บรรทัดเดียวกับ class handler รับ c *gin.Context
def indexเมนู (action) · ชื่อต้องตรงกับขวาของ # ใน route · ไม่มี argument handler หนึ่งตัว
params[:name]รายละเอียดในใบสั่ง · มาได้ทั้ง ?name=Somchai และ /hello/:name ลงกล่องเดียวกัน · เป็นข้อความเสมอ (อยากได้เลขใช้ .to_i) c.Query("name") · c.Param("name")
?name= (มี = แต่ว่าง)ได้ "" ไม่ใช่ nil → "" || "PEA" ได้ "" · แก้ด้วย .presence เช็ค name == "" ตรง ๆ
@name = …ถาดหน้าครัว — ทางเดียว ที่ view มองเห็น · ลืม @ ได้ 200 กับค่าว่าง ไม่มี error c.HTML(200, "index.tmpl", gin.H{"name": name})
ไม่ได้สั่ง render Rails เปิด app/views/<แผนก>/<เมนู>.html.erb ให้เอง (implicit render) ต้องสั่งตอบเองทุกครั้ง
render plain: "…"ตอบเป็นข้อความเปล่า ไม่ใช้ไฟล์ view c.String(…)
before_action :set_post, only: %i[show edit update]“ก่อนเข้าเมนูพวกนี้ เรียก set_post ก่อน” · %i[…] = [:show, :edit, :update] · method ใต้ private ไม่ใช่เมนู middleware ที่ผูกบาง route
render (ยกจาน) กับ redirect_to (ชี้ไปเคาน์เตอร์อื่น)
renderredirect_to "/hello"
จำนวน request 1 รอบ จบ 2 รอบ (browser ยิงรอบสองเอง)
ที่เห็นใน curl -i HTTP/1.1 200 OK + หน้าเว็บHTTP/1.1 302 Found + location: http://localhost:3000/hello · content-length: 0
URL บนแถบที่อยู่ ไม่เปลี่ยน เปลี่ยนเป็นที่อยู่ใหม่
@ตัวแปรใช้ต่อใน view ได้ หายหมด — อยากส่งต่อใส่ใน URL เช่น "/hello?name=Rails"
ใช้ตอนไหน กรอกฟอร์มไม่ผ่าน → โชว์ฟอร์มเดิม (ของที่พิมพ์ยังอยู่) ทำสำเร็จ → พาไปหน้าผลลัพธ์ (refresh แล้วไม่ส่งซ้ำ)
ใน Gin c.HTML(…) · c.String(…)c.Redirect(http.StatusFound, "/hello")
เลขสถานะที่เจอใน log
เลข ชื่อใน Rails ใน Go ในร้านอาหาร เจอตอนไหน
200 :okhttp.StatusOKจานถึงโต๊ะเรียบร้อย ปกติ
302 :foundhttp.StatusFound“เมนูนี้ไปสั่งเคาน์เตอร์ 2 นะคะ” redirect_to
403 :forbiddenhttp.StatusForbiddenด่านหน้าครัวไม่ให้เข้า app.get ใน console ก่อน app.host!
404 :not_foundhttp.StatusNotFoundร้านไม่มีเมนูนี้ ไม่มี route · ไม่มี action
406 :not_acceptablehttp.StatusNotAcceptableเชฟทำเสร็จแต่หาแบบจัดจานไม่เจอ ไม่มีไฟล์ view (เปิดด้วย browser)
422 :unprocessable_contenthttp.StatusUnprocessableEntityใบสั่งใช้ไม่ได้ ส่งคืน ไม่มี CSRF token · ฟอร์มไม่ผ่าน (Day 2)
500 :internal_server_errorhttp.StatusInternalServerErrorครัวไฟไหม้ — ความผิดของร้าน โค้ดเรา error · ต่อ database ไม่ติด
อ่าน log ของ server ทีละบรรทัด
บรรทัดใน log แปลว่า ในร้านอาหาร
Started GET "/posts" for ::1request เข้ามา (::1 = เครื่องตัวเอง) ด่านหน้าครัวจดใบสั่งใหม่
Processing by PostsController#index as HTMLroutes จับคู่ได้ ส่งถึง action แล้ว · ไม่มีบรรทัดนี้ = routes หาไม่เจอ หัวหน้าเชฟรับใบสั่ง
Parameters: {"name" => "Somchai"}ของในกล่อง params รายละเอียดในใบสั่ง
Rendering posts/index.html.erb within layouts/applicationสร้าง HTML จาก view วางใน layout จัดจานบนจานรอง
Redirected to http://localhost:3000/postsสั่ง redirect · ไม่มีบรรทัด Rendering ชี้ไปเคาน์เตอร์อื่น
Completed 200 OK in 18msตอบแล้ว · เลขสถานะ · เวลารวม · ActiveRecord: 0.0ms = ไม่ได้แตะ database ยกจานถึงโต๊ะ
10 · view · ERB · layout · link_to · ความปลอดภัยที่ได้ฟรี (บท 103)
ไฟล์ view ลงท้าย .html.erb — ERB = HTML ธรรมดาที่เจาะช่องให้ Ruby เติมได้ ·
เหมือนใบแบบจัดจานที่เว้นช่อง “ใส่ชื่อเมนูตรงนี้” ไว้ · ช่องมีสามแบบตามตารางนี้
แท็ก ERB ทำอะไร ลูกค้าเห็นไหม ใน Go html/template
<%= … %>รัน Ruby แล้วพิมพ์ผล ลงตรงนั้น (= = “เอาออกไปโชว์”) ✅ เห็นผล {{.Title}}
<% … %>รันเฉย ๆ ไม่พิมพ์ — ใช้กับ if · each · end ❌ ตัวมันไม่เห็น (HTML ที่มันคุมเห็นได้) {{if …}} · {{range .}} · {{end}}
<%# … %>comment ไม่รันเลย ❌ ไม่เห็นสักตัว —
วนรายการบทความ (จำลองด้วย Hash — มี model จริงแล้วเขียน post.title ได้ใน Day 2):
<h1>บทความทั้งหมด</h1>
<ul>
<% @posts.each do |post| %> <%# ไม่มี = เพราะเป็นคำสั่งถึงคนจัดจาน %>
<li><%= link_to post[:title], post_path(post[:id]) %></li>
<% end %>
</ul>
ผลจริงหนึ่งแถว: <li><a href="/posts/1">ข้าวมันไก่ร้านดัง</a></li> ·
link_to ข้อความ, ที่อยู่ สร้างแท็ก <a> · ใช้ post_path แทนพิมพ์ URL เอง
กับดัก ERB เห็นอะไร แก้
ลืม = เช่น <% @post[:id] %> ช่องนั้นว่าง ไม่มี error ใส่ =
ใส่ = ที่บรรทัด each <%= @posts.each do |post| %> ข้อมูลดิบทั้งก้อน [{id: 1, …}] โผล่ท้ายรายการ ลบ =
@post.title ทั้งที่ @post เป็น Hash จำลองundefined method 'title' for an instance of Hash@post[:title] (แบบจุดใช้ได้ Day 2)
layout — จานรองแบบเดียวกันทุกจาน
<%# app/views/layouts/application.html.erb (ของจริงจาก rails new · ย่อ) %>
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Wongnok Blog" %></title>
<%= csrf_meta_tags %>
</head>
<body>
<main class="container mx-auto mt-28 px-5 flex">
<%= yield %> <%# ← view ของแต่ละหน้าถูกวางตรงนี้ %>
</main>
</body>
</html>
view ที่เราเขียนไม่ต้องมี <html> · Rails วางลงตรง <%= yield %> ให้เอง (เหมือน {children} ใน app/layout.tsx ของ Next.js) ·
Go ไม่มี layout ให้อัตโนมัติ · ในโหมด development Rails แปะ comment <!-- BEGIN app/views/… --> บอกว่า HTML ท่อนนี้มาจากไฟล์ไหน
ความปลอดภัยที่ได้ฟรี — ไม่ต้องเขียนอะไรเพิ่ม
ภัย ภาษาคน Rails ทำให้แล้ว (ผลจริง) ปิดตัวกันเองถ้า…
XSS คนร้ายตั้งชื่อบทความเป็น <script>… ให้ไปรันใน browser คนอื่น ทุก <%= %> แปลง < → < · > → > · ' → ' · จอโชว์ตัวอักษร แต่ไม่รัน (Go html/template ก็ทำแบบนี้) ใช้ raw หรือ .html_safe กับข้อมูลที่ผู้ใช้พิมพ์ — ได้ <script> ตัวจริง ไม่มีใครเตือน
CSRF เว็บคนร้ายแอบส่งคำสั่ง “ลบบทความ” มา โดยอาศัย cookie ของเรา ทุกหน้ามีรหัสลับ csrf-token ใน <head> · คำสั่งที่ไม่ใช่ GET ไม่แนบรหัส → 422 + Can't verify CSRF token authenticity. · action ไม่ได้ทำงานเลย ปิดการตรวจเอง — ฟอร์มและปุ่มของ Rails แนบรหัสให้อยู่แล้ว (Day 2–3)
11 · debugger · console (บท 104)
เครื่องมือ ภาษาคน ในร้านอาหาร ใช้เมื่อ ใน Go
puts · p ใน actionพิมพ์ค่า · ผลขึ้นใน terminal ของ server ไม่ใช่บนหน้าเว็บ กล้องที่ติดไว้ไม่กี่มุม รู้อยู่แล้วว่าจะดูค่าไหน fmt.Println
debuggerหยุดโค้ดบรรทัดนั้น แล้วถามค่าอะไรก็ได้ กดหยุดเวลาในครัว ต้องมีใบสั่งวิ่งมา เช่นอยากดู params Delve (dlv)
bin/rails consoleโหลดแอปทั้งตัว ให้พิมพ์ทีละบรรทัด ไม่ต้องเปิด server ครัวทดลองหลังร้าน ลองชื่อ route · helper · แปลงชื่อ ไม่มี
bin/rails console --sandboxเหมือน console แต่ทุกการเปลี่ยนใน database ถูกยกเลิกตอน exit เขียนบนแผ่นใสที่วางทับแฟ้ม ลองคำสั่งน่ากลัวกับข้อมูล (Day 2) defer tx.Rollback()
พิมพ์ได้ตอน debugger หยุด (prompt (rdbg))
พิมพ์ ย่อ ทำอะไร
ชื่อตัวแปร / โค้ด Ruby — ดูค่าทันที เช่น @name · params[:name]
nextnทำบรรทัดนี้ แล้วหยุดบรรทัดถัดไป · ลูกศร => ชี้บรรทัดที่ “ยังไม่ทำ”
stepsมุดเข้าไปใน method ที่บรรทัดนี้เรียก (ระวังหลงเข้าโค้ดของ Rails)
info— ดูตัวแปรทุกตัว · ที่ขึ้นต้น @_ เป็นของ Rails
continuecปล่อยวิ่งต่อ · หน้าเว็บจะขึ้น (ระหว่างหยุด browser หมุนค้าง)
ใช้ใน console เท่านั้น (ผลจริง)
พิมพ์ ได้ ภาษาคน
app.hello_path"/hello"app = browser จำลอง · ถามชื่อ route ได้
helper.link_to "หน้าทักทาย", "/hello""<a href=\"/hello\">หน้าทักทาย</a>"helper = เรียก helper ของ view
app.get "/hello?name=Somchai"=> 403 + Blocked hosts: www.example.comติดด่านเช็คชื่อเว็บ — ตั้งใจ ไม่ใช่บั๊ก
app.host! "localhost" แล้ว app.get …=> 200บอก browser จำลองให้ใช้ชื่อ localhost (ตั้งครั้งเดียวต่อการเปิด console)
app.response.bodyHTML ทั้งหน้า ดูจานที่ได้
reload!Reloading... · => nilแก้ไฟล์ใน app/ ระหว่าง console เปิด ต้องสั่งก่อนลองใหม่
⚠ ลบ debugger ก่อน commit ทุกครั้ง
gem debug อยู่ใน group :development, :test — เว็บจริง (production) ไม่โหลด ·
ลืมไว้ = ทุกคนที่เปิดหน้านั้นได้ 500 (NameError (undefined local variable or method 'debugger' …)) ·
ก่อน commit รัน grep -rn debugger app/ ให้ว่างเปล่า · และเปิด server ด้วย bin/rails server ไม่ใช่ bin/dev ตอนจะใช้ debugger
12 · ตาราง error → แปลว่า → แก้ยังไง
อ่านหน้าแดง 3 จังหวะ (บท 105): ① ชื่อ error + เกิดที่แผนกไหน (NameError in PagesController#about) →
② ประโยคตัวแดง + Did you mean? → ③ หา path ที่ขึ้นต้นด้วย app/ กับเลขบรรทัด · ถ้าไม่มีไฟล์ของเราเลย = ปัญหาอยู่ที่ชื่อ (route · ชื่อไฟล์ · ชื่อ class)
Ruby (ไฟล์ .rb และในแอป)
ข้อความที่เห็น แปลว่า แก้ยังไง
undefined local variable or method 'titel' for main (NameError) + Did you mean? titleไม่รู้จักชื่อนี้ สะกดผิด — ดูชื่อที่ Ruby เดาให้
undefined method 'upcase' for nil (NoMethodError)ค่าตรงนั้นเป็น nil (กล่องว่าง) อย่าแก้ที่บรรทัดที่พัง — ย้อนหาว่าทำไมเป็น nil · หรือ &.
undefined method 'upcase' for an instance of Integer (NoMethodError)ของในมือเป็นชนิดอื่น (ตัวเลข) ไม่มีคำสั่งนี้ ดูท่อน for an instance of … ว่าเป็นอะไรจริง
undefined method 'days' for an instance of Integer · … 'blank?' for an instance of Stringเป็นของ Active Support ไม่ใช่ Ruby เปล่า ใช้ในแอป Rails หรือรัน ruby -ractive_support/all ไฟล์.rb
undefined method 'title' for an instance of Hash (NoMethodError)ใช้ .title หรือ &:title กับ Hash post[:title] · { |post| post[:title] }
undefined method 'title' for an instance of Postclass ของเราไม่มี method อ่านค่า เติม attr_accessor :title (หรือ attr_reader)
undefined method 'length' for nil หลัง upcase!upcase! ไม่มีอะไรต้องเปลี่ยน เลยคืน nilเรียก method ! เป็นบรรทัดเดี่ยว ไม่เอาค่าไปต่อ
no implicit conversion of Integer into String (TypeError)ต่อข้อความกับตัวเลขด้วย + "อ่าน #{views}" หรือ views.to_s
nil can't be coerced into Integer (TypeError)เอา nil ไปบวก (มักหยิบ key ผิดชนิด post["views"]) หยิบด้วย symbol post[:views]
key not found: :author (KeyError)fetch ช่องที่ไม่มีเพิ่มช่อง หรือให้ค่าสำรอง fetch(:author, "ไม่ระบุ")
missing keyword: :title (ArgumentError)ลืมส่ง keyword ที่บังคับ ส่ง title: … · ดู caller (บรรทัดที่เรียก) กับ callee (บรรทัดที่ประกาศ)
wrong number of arguments (given 2, expected 1) (ArgumentError)ส่งจำนวน argument ไม่ตรง นับให้ตรง · ใน initialize ของลูกใช้ super(title) ไม่ใช่ super เปล่า
wrong number of arguments (given 1, expected 0; required keyword: model)ใส่ปีกกาให้ของที่ต้องส่งแบบ keyword form_with model: post do |f| ไม่มีปีกกา
no block given (yield) (LocalJumpError)method มี yield แต่ไม่ได้แนบ block เรียกพร้อม do … end
uninitialized constant Post::Slugable (NameError) + Did you mean? Sluggableชื่อตัวใหญ่ (class · module) ที่ไม่รู้จัก สะกดให้ตรง
syntax errors found + expected an `end` to close the `def` statementลืม end · บรรทัดที่ชี้อาจไม่ใช่บรรทัดที่ลืม ไล่คู่ def/class/do กับ end · ตรวจด้วย ruby -c
divided by 0 (ZeroDivisionError)หารด้วยศูนย์ ไม่มีใครรับ โปรแกรมหยุด เช็คก่อน หรือ rescue ZeroDivisionError
warning: already initialized constant BLOG_NAMEตั้งค่าคงที่ซ้ำ (เปลี่ยนแล้วจริง แค่เตือน) ลบบรรทัดที่ตั้งซ้ำ
#<Enumerator:0x…> โผล่แทนผลputs … do … end — block ไปติดกับ putsใช้ { } หรือใส่วงเล็บ puts( … )
method ได้ nil ทั้งที่คำนวณถูก บรรทัดสุดท้ายเป็น puts ให้ค่าที่อยากส่งกลับอยู่บรรทัดสุดท้าย
Rails (หน้าแดง · log ของ server · terminal)
ข้อความที่เห็น status แปลว่า แก้ที่ไหน
ActiveRecord::ConnectionNotEstablished (connection to server on socket "/tmp/.s.PGSQL.5432" failed …500 ไม่รู้ว่า PostgreSQL อยู่ไหน / Postgres ไม่เปิด export DATABASE_URL=… ในหน้าต่างเดียวกับ server
ActiveRecord::NoDatabaseError (Database not found: …)500 ยังไม่ได้สร้าง database ปิด server → bin/rails db:prepare → เปิดใหม่
ActiveRecord::DatabaseConnectionError: There is an issue connecting with your hostname: 127.0.0.1.— ต่อ database ไม่ได้ Postgres เปิดไหม · พอร์ตใน DATABASE_URL · หน้าต่างใหม่ต้อง export ซ้ำ
A server is already running (pid: …)— มี server เปิดค้างอีกหน้าต่าง ไปกด Ctrl-C ที่หน้าต่างนั้น
Routing Error · No route matches [GET] "/comments"404 พนักงานต้อนรับไม่มีกฎนี้ (log ไม่มี Processing by) config/routes.rb · save แล้วหรือยัง
Unknown action · The action 'popular' could not be found for PostsController404 route มี แต่แผนกไม่มีเมนูนี้ เพิ่ม def popular ใน controller
ActionDispatch::MissingController in PageController#about · uninitialized constant PageController500 หา controller ชื่อนี้ไม่เจอ (มักขาด s) ให้ตรงกันสามที่: คำใน route · ชื่อไฟล์ · ชื่อ class
No view template for interactive request · PagesController#about is missing a template for request formats: text/html406 หาไฟล์ view ไม่เจอ ชื่อไฟล์/โฟลเดอร์ใน app/views/ — ย่อหน้า NOTE บอกชื่อที่ถูกให้
NameError in PagesController#about · undefined local variable or method 'parms'500 พิมพ์ชื่อผิดในโค้ด บรรทัดที่กรอบโค้ดชี้ · ดู Did you mean?
ArgumentError · Reported error must be an Exception, got: #<SyntaxError:…500 หัวข้อหลอกตา — ตัวจริงคือ SyntaxError ข้างใน (มักลืม end) อ่านใน terminal ของ server (Caused by: SyntaxError) · ruby -c ไฟล์
syntax error found · unexpected '<', ignoring it500 < ApplicationController อยู่คนละบรรทัดกับ classย้ายขึ้นบรรทัดเดียวกัน
Invalid route name, already in use: 'root' (ArgumentError)500 เขียน root สองบรรทัด เหลือบรรทัดเดียว
undefined method 'post_path'500 route นั้นไม่มีชื่อ / ไม่ได้เปิด :show ใส่ as: :post หรือ only: [:index, :show]
ActionView::Template::Error (undefined method 'each' for nil)500 view หา @posts ไม่เจอ ชื่อ @… ใน controller ให้ตรงกับ view (เช่นลืม s)
หน้าขึ้นปกติแต่ค่าหาย (Hello ) 200 @ตัวแปร สะกดไม่ตรง · ลืม @ · ลืม = ใน ERB · หรือ params key ผิดเทียบชื่อระหว่าง controller กับ view · ใช้ debugger ดู
PG::UndefinedTable: ERROR: relation "categories" does not exist— มีใบสั่งช่าง (migration) แต่ยังไม่สั่งลงมือ bin/rails db:migrate
uninitialized constant Post (NameError) ใน console— ไม่มีไฟล์ app/models/post.rb หรือชื่อไฟล์ไม่ตรง class ชื่อไฟล์ = ชื่อ class ตัวเล็ก
ApplicationRecord is an abstract class and cannot be instantiated.— ใช้แม่แบบกลางสร้างของตรง ๆ ใช้ model จริง เช่น Post.new
Can't verify CSRF token authenticity. · ActionController::InvalidAuthenticityToken422 คำสั่งที่ไม่ใช่ GET ไม่ได้แนบรหัสลับ ใช้ฟอร์ม/ปุ่มของ Rails (แนบให้เอง) — ยิง curl เปล่า ๆ ได้ 422 ไม่ใช่บั๊ก
ActionNotFound … but it is listed in the controller's :only option404 ใส่ action ใน only: ที่ยังไม่ได้เขียน ใส่เฉพาะ action ที่เขียนแล้ว
NameError (undefined local variable or method 'debugger' …)500 ลืม debugger ไว้ แล้วรันในโหมดที่ไม่มี gem debug ลบ · grep -rn debugger app/
DEBUGGER: wait for debugger connection... แล้วพิมพ์อะไรก็ไม่ตอบ— เปิดด้วย bin/dev Ctrl-C แล้วเปิดด้วย bin/rails server
Blocked hosts: www.example.com · => 403403 app.get ใน console ติดด่านชื่อเว็บapp.host! "localhost"
eval error: undefined local variable or method 'app'— พิมพ์ app ใน (rdbg) ของ debugger app · helper มีเฉพาะใน bin/rails console
URI must be ascii only— ใส่ตัวอักษรไทยใน URL ของ app.get ใช้ชื่อภาษาอังกฤษ
ArgumentError: Missing host to link to!— เรียก post_url ใน bin/rails runner (ไม่มีใบสั่งให้ยืมโดเมน) ใช้ใน controller/view หรือใช้ _path
fatal: your current branch 'main' does not have any commits yet— ไม่ใช่ error จริง — rails new ไม่ได้ commit ให้ git add -A · git commit -m "rails new"
13 · ศัพท์ภาษาคน
ศัพท์ ภาษาคน ในร้านอาหาร ใน Go
framework ชุดเครื่องมือทำเว็บสำเร็จรูป ที่เป็นฝ่ายเรียกโค้ดของเรา ร้านแฟรนไชส์ Gin (แต่ Rails ให้มาครบกว่ามาก)
Ruby · Rails Ruby = ภาษา · Rails = framework ที่เขียนด้วย Ruby ภาษาที่พูดในครัว · ตัวร้าน Go · Gin
MVC แบ่งโค้ดเป็น ข้อมูล (Model) · หน้าตา (View) · ผู้ประสานงาน (Controller) ห้องวัตถุดิบ · คนจัดจาน · หัวหน้าเชฟ handler เดียวทำทั้งสามงาน
Convention over Configuration ตั้งชื่อตามกฎ แทนเขียนตั้งค่าเอง ทุกสาขาวางของที่เดิม ต้องบอกทุกอย่างเอง
gem · Bundler ชุดโค้ดที่คนอื่นเขียนให้ลงมาใช้ · ตัวติดตั้ง gem ตาม Gemfile อุปกรณ์สำเร็จรูป · คนถือรายการไปซื้อ module · go mod
Active Support ส่วนของ Rails ที่เติม method ให้ของพื้นฐานของ Ruby (blank? · 3.days.ago) สมุดศัพท์ประจำร้าน ไม่มี
object · method ค่าที่มีคำสั่งติดตัว · คำสั่งแต่ละตัว เรียกด้วยจุด ของในบ้านที่มีปุ่มติดมา Go: ชนิดพื้นฐานไม่มี method
symbol ป้ายชื่อที่ขึ้นต้นด้วย : เช่น :title ป้ายลิ้นชัก ใกล้สุดคือ const string
block · |x| · yield ก้อนโค้ดที่ฝากไปกับ method · ชื่อของที่ส่งเข้ามา · คำสั่งเรียกก้อนนั้น สูตรที่ฝากไว้กับแม่ครัว func ที่ส่งเป็น argument
truthy · falsy ค่าที่ผ่าน / ไม่ผ่าน if · Ruby ไม่ผ่านแค่ nil กับ false รปภ. ที่ห้ามแค่ 2 คน if รับแค่ bool
instance variable (@x) ตัวแปรที่ติดอยู่กับ object · ใน controller คือทางส่งของให้ view ถาดหน้าครัว field ของ struct · gin.H{…}
inheritance (<) · superclass class ลูกได้ทุกอย่างของ class แม่ · แม่เรียก superclass คู่มือบริษัทแม่ → คู่มือสาขา → แผนก ไม่มี (ใช้ฝัง struct)
module · mixin (include) · concern ก้อน method ที่หลาย class ยืมไปใช้ · concern = module ที่วางใน concerns/ ชุดเครื่องมือกลางของครัว embedding
exception · rescue · raise error ที่ถูกโยน · ที่รับ · การโยนเอง กริ่งไฟไหม้ · เจ้าหน้าที่ประจำชั้น · กดกริ่งเอง error ที่ return · if err != nil · errors.New
duck typing ใช้ได้ถ้า “ทำได้” (มี method) ไม่สนว่าเป็น class อะไร ร้องก๊าบ ๆ ได้ ก็ถือว่าเป็นเป็ด interface ที่ compiler ตรวจ
DSL โค้ดที่อ่านเหมือนภาษาเฉพาะเรื่อง — จริง ๆ คือเรียก method ที่ละวงเล็บ ใบสั่งแบบพูดย่อ —
route · controller#action กฎหนึ่งบรรทัด จับคู่ verb + path กับแผนก + เมนู รายการในมือพนักงานต้อนรับ r.GET(path, handler)
params ทุกอย่างที่ผู้ใช้ส่งมากับ request รวมไว้กล่องเดียว · เป็นข้อความเสมอ รายละเอียดในใบสั่ง c.Param · c.Query
implicit render action จบโดยไม่สั่ง → Rails เปิด view ชื่อตรงกันให้ เชฟวางของเสร็จ คนจัดจานรู้เองว่าจัดจานไหน ต้องสั่ง c.HTML เอง
ERB · layout · helper HTML ที่แทรก Ruby ได้ · กรอบที่ครอบทุกหน้า · method สำเร็จรูปที่คืน HTML ใบแบบจัดจาน · จานรอง · เครื่องพิมพ์ป้าย html/template · จัดเอง · ต่อข้อความเอง
named route · _path · _url route ที่มีชื่อ · ได้ method สร้าง path / URL เต็ม ชื่อเล่นโต๊ะ fmt.Sprintf เอง
resources · CRUD route มาตรฐาน 8 เส้น · สร้าง อ่าน แก้ ลบ เซ็ตเมนูมาตรฐานของแฟรนไชส์ เขียน r.GET r.POST … เองทีละเส้น
middleware โค้ดที่ทุก request ต้องผ่านก่อนถึง routes (ขาเข้าและขากลับ) ด่านหน้าครัว r.Use(…)
environment โหมดที่แอปรัน · development = เครื่องเรา · production = เว็บจริง สาขาทดลอง · ร้านจริง —
autoload (Zeitwerk) Rails เปิดไฟล์ให้เองจากชื่อ class ไม่ต้อง import · ชื่อไฟล์ต้องตรงชื่อ class บรรณารักษ์ที่อ่านแค่สันหนังสือ import "blog/models"
model · attribute class ที่แทนข้อมูลหนึ่งชนิด · ช่องข้อมูลที่ Rails สร้าง method ให้จากคอลัมน์ คนดูแลสต็อก · ช่องในแฟ้ม struct · field
migration · schema ไฟล์สั่งสร้าง/แก้ตาราง · db/schema.rb สรุปโครงตารางทั้งหมด ใบสั่งช่างต่อเติมตู้ · ผังตู้ goose · ไม่มี
XSS · escape ยัดโปรแกรมลงในข้อมูลให้ไปรันที่ browser คนอื่น · แปลง < เป็นตัวอักษรธรรมดา คนจัดจานที่ไม่ทำตามคำสั่งแปลก ๆ ในใบสั่ง html/template escape ให้เหมือนกัน
CSRF · authenticity token ปลอมคำสั่งจากเว็บอื่นโดยอาศัย cookie · รหัสลับที่ Rails ฝังในหน้าเว็บ บัตรคิวที่แจกเฉพาะลูกค้าในร้าน ต้องหาเครื่องมือมาเสียบเอง
breakpoint · backtrace จุดที่สั่งให้หยุด · รายการชั้นการเรียกที่พามาถึงจุดพัง (Application Trace = ไฟล์ของเรา) จุดหยุดเวลา · เส้นทางใบสั่ง Delve · stack trace ของ panic
REPL อ่าน → รัน → โชว์ผล → วนใหม่ · irb และ bin/rails console กระดาษทด · ครัวทดลอง ไม่มี
14 · ถ้าเปิดสไลด์ดูประกอบ
เฉพาะจุดที่พิมพ์ตามสไลด์แล้วพัง · ที่เหลือดูได้ในตารางท้ายแต่ละบท
หน้า สไลด์เขียน ของจริง ทำยังไง
21 nums.min / nums.max · nums.sort / nums.uniq/ คือหาร · ได้ 0 และ NoMethodErrorแยกพิมพ์ทีละบรรทัด
20–21 posts.map(&:title)posts เป็น Hash จะพัง undefined method 'title' for an instance of Hash ใช้ { |post| post[:title] } จน Day 2
29 Post.new.slug # ใช้ได้เลยNameError — Post ในสไลด์ไม่มี titleเติม attr_accessor :title + initialize
40 “git repo ที่ commit แรกให้แล้ว” · พิมพ์ bin/rails server ต่อได้เลย ไม่ได้ commit · หน้าแรกได้ 500 ถ้าไม่บอกที่อยู่ Postgres commit เอง · ตั้ง DATABASE_URL + db:prepare
60 class GreetingsController กับ < ApplicationController คนละบรรทัดSyntaxError · หน้าเว็บ 500เขียนบรรทัดเดียวกัน
63 only: %i[show edit update] แต่โชว์แค่ def showทุก URL ของ controller นั้นได้ 404 ใส่ใน only: เฉพาะ action ที่เขียนแล้ว
64 stylesheet_link_tag "application"ไฟล์จริงใช้ :app (รวม Tailwind) อย่าแก้ตามสไลด์
68 app.get "/posts" ได้ => 200ได้ 403 + Blocked hosts app.host! "localhost" ก่อน
15 · อ้างอิง
← กลับไปบทเรียน 90–105 🏠 หน้าแรก